Language-specific Default URL using MVC's Attribute Routing and RouteLocalization.mvc - asp.net

I would like to be able to create a succinct language-specific default URL for my website so that when someone browses to:
somesite.com
They get redirected to a language-culture page such as:
somesite.com/en-US/
somesite.com/sp-MX/
somesite.com/fr-FR/
Specifically, I do not want /Home/Index appended to the URLs:
somesite.com/en-US/Home/Index
somesite.com/sp-MX/Home/Index
somesite.com/fr-FR/Home/Index
I am committed to making this site using RouteLocalization.mvc
Dresel/RouteLocalization
Translating Your ASP.NET MVC Routes
And I would like to use MVC Attribute Routing to the extent feasible.
I am having trouble figuring out how to cause the Start() method redirect to a language-culture specific URL without the addition of something like "index".
Samples of what I have attempted follow:
using RouteLocalization.Mvc;
using RouteLocalization.Mvc.Extensions;
using RouteLocalization.Mvc.Setup;
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.Clear();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes(Localization.LocalizationDirectRouteProvider);
const string en = "en-us";
ISet<string> acceptedCultures = new HashSet<string>() { en, "de", "fr", "es", "it" };
routes.Localization(configuration =>
{
configuration.DefaultCulture = en;
configuration.AcceptedCultures = acceptedCultures;
configuration.AttributeRouteProcessing = AttributeRouteProcessing.AddAsNeutralAndDefaultCultureRoute;
configuration.AddCultureAsRoutePrefix = true;
configuration.AddTranslationToSimiliarUrls = true;
}).TranslateInitialAttributeRoutes().Translate(localization =>
{
localization.AddRoutesTranslation();
});
CultureSensitiveHttpModule.GetCultureFromHttpContextDelegate = Localization.DetectCultureFromBrowserUserLanguages(acceptedCultures, en);
var defaultCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
routes.MapRoute(
name: "DefaultLocalized",
url: "{culture}/{controller}/{action}/{id}",
constraints: new { culture = #"(\w{2})|(\w{2}-\w{2})" },
defaults: new { culture = defaultCulture, controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
And my home controller:
public class HomeController : Controller
{
[HttpGet]
[Route]
public RedirectToRouteResult Start()
{
return RedirectToAction("Home", new { culture = Thread.CurrentThread.CurrentCulture.Name });
}
[Route("Index", Name = "Home.Index")]
public ActionResult Index()
{
return View();
}
public ActionResult Contact()
{
return View();
}
public ActionResult About()
{
return View();
}
}
My Global.asax file:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
AreaRegistration.RegisterAllAreas();
}
}

Redirecting is a separate concern than routing. Since your goal of redirecting any URL to its localized counterpart is a cross-cutting concern your best bet is to make a global filter.
public class RedirectToUserLanguageFilter : IActionFilter
{
private readonly string defaultCulture;
private readonly IEnumerable<string> supportedCultures;
public RedirectToUserLanguageFilter(string defaultCulture, IEnumerable<string> supportedCultures)
{
if (string.IsNullOrEmpty(defaultCulture))
throw new ArgumentNullException("defaultCulture");
if (supportedCultures == null || !supportedCultures.Any())
throw new ArgumentNullException("supportedCultures");
this.defaultCulture = defaultCulture;
this.supportedCultures = supportedCultures;
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var routeValues = filterContext.RequestContext.RouteData.Values;
// If there is no value for culture, redirect
if (routeValues != null && !routeValues.ContainsKey("culture"))
{
string culture = this.defaultCulture;
var userLanguages = filterContext.HttpContext.Request.UserLanguages;
if (userLanguages.Length > 0)
{
foreach (string language in userLanguages.SelectMany(x => x.Split(';')))
{
// Check whether language is supported before setting it.
if (supportedCultures.Contains(language))
{
culture = language;
break;
}
}
}
// Add the culture to the route values
routeValues.Add("culture", culture);
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 RedirectToUserLanguageFilter("en", new string[] { "en", "de", "fr", "es", "it" }));
filters.Add(new HandleErrorAttribute());
}
}
Note also that your routing is misconfigured. The route setup is run one time per application startup, so setting the default culture to that of the current thread is meaningless. In fact, you should not be setting a default culture at all for your culture route because you want it to miss so your Default route will execute if there is no culture set.
routes.MapRoute(
name: "DefaultLocalized",
url: "{culture}/{controller}/{action}/{id}",
constraints: new { culture = #"(\w{2})|(\w{2}-\w{2})" },
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Related

Having issues generating URLs for custom routes to include the users culture in the RouteValues

I'm trying to create a custom route which will include the users culture in the route (RouteValues). Using default routing convention everything works fine.
I have the following controller:
public class HomeController : Controller
{
public HomeController()
{
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
}
I believe I've configured localization in the app correctly as follows:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RequestLocalizationOptions>(opts =>
{
opts.SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fr") };
opts.SupportedUICultures = opts.SupportedCultures;
opts.SetDefaultCulture("en");
opts.DefaultRequestCulture = new RequestCulture("en");
opts.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider);
opts.ApplyCurrentCultureToResponseHeaders = opts.ApplyCurrentCultureToResponseHeaders;
});
services.AddControllersWithViews(opts =>
{
opts.Filters.Add(new CultureFilter("en"));
});
services.AddLocalization();
services.AddMvc();
}
And I have an ActionFilter that sets the users culture based on the route value.
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var values = context.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);
}
}
Using the default routing convention I get the desired result (more or less).
Thus for the following routes:
endpoints.MapControllerRoute(
name: "culture-default",
pattern: "{culture=en}/{controller=Home}/{action=Index}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}");
Thus while on the url "http://localhost" I get:
"#Url.ActionLink("Index", "Home")" = "http://localhost"
"#Url.ActionLink("Privacy", "Home")" = "http://localhost/home/privacy"
And while on the url "http://localhost/fr" I get:
"#Url.ActionLink("Index", "Home")" = "http://localhost/fr"
"#Url.ActionLink("Privacy", "Home")" = "http://localhost/fr/home/privacy"
Ok - so far so goood....
But when I add a custom route for the Privacy ActionMethod I can't seem to get the correct culture in the generated URL.
Thus for the following routes:
endpoints.MapControllerRoute(
name: "culture-privacy",
pattern: "{culture}/h/p",
defaults: new { culture = "en", controller = "Home", action = "Privacy" });
endpoints.MapControllerRoute(
name: "default-privacy",
pattern: "h/p",
defaults: new { controller = "Home", action = "Privacy" });
Thus while on the url "//localhost" I get:
"#Url.ActionLink("Index", "Home")" = "//localhost"
"#Url.ActionLink("Privacy", "Home")" = "//localhost/h/p"
And while on the url "//localhost/fr" I get:
"#Url.ActionLink("Index", "Home")" = "//localhost/fr"
"#Url.ActionLink("Privacy", "Home")" = "//localhost/en/h/p"
Presumably this is because I included the "culture = 'en'" in the default for the "culture-privacy" route, but shouldn't the default value of 'en' only be used if the culture is not otherwise specified in the route?
You are right, the reason is that the default value "en" is overriding the value passed in the route data. If you want to use the culture which user typed, try to use the culture value passed in the route data instead of a fixed value. Here is the code sample:
endpoints.MapControllerRoute(
name: "culture-privacy",
pattern: "{culture}/h/p",
defaults: new { culture = "{culture}", controller = "Home", action = "Privacy" });

How to build routes only to action and to controller with id?

I'm trying to build two routes only to action and to controller with id, keeping the default.
I have to access:
www.mysite.com/MyController/MyAction/{OptionalId}
www.mysite.com/MyController/{OptionalId}
www.mysite.com/MyActionFromHomeController
I was able to create routes to work with first and the third point, but not for the second. Current code:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "OnlyActionToHomeController",
url: "{action}",
defaults: new { controller = "Home" },
constraints: new { noConflictingControllerExists = new NoConflictingControllerExists() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
public class NoConflictingControllerExists : IRouteConstraint
{
private static readonly Dictionary<string, bool> _cache = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var path = httpContext.Request.Path;
if (path == "/" || String.IsNullOrEmpty(path))
return false;
if (_cache.ContainsKey(path))
return _cache[path];
IController ctrl;
try
{
var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
ctrl = ctrlFactory.CreateController(httpContext.Request.RequestContext, values["action"] as string);
}
catch
{
_cache.Add(path, true);
return true;
}
var res = ctrl == null;
_cache.Add(path, res);
return res;
}
}
I did!
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "OnlyController",
url: "{controller}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { id = #"\d+" } // define the id parameter needs to be integer
);
routes.MapRoute(
name: "OnlyActionToHomeController",
url: "{action}",
defaults: new { controller = "Home" },
constraints: new { noConflictingControllerExists = new NoConflictingControllerExists() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
public class NoConflictingControllerExists : IRouteConstraint
{
private static readonly Dictionary<string, bool> _cache = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var path = httpContext.Request.Path;
if (path == "/" || String.IsNullOrEmpty(path))
return false;
if (_cache.ContainsKey(path))
return _cache[path];
IController ctrl;
try
{
var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
ctrl = ctrlFactory.CreateController(httpContext.Request.RequestContext, values["action"] as string);
}
catch
{
_cache.Add(path, true);
return true;
}
var res = ctrl == null;
_cache.Add(path, res);
return res;
}
}

Asp .net MVC 5 Owin authentication with multiple login pages

I have an asp.net MVC 5 site with OWIN authentication. I would like to have two different log in pages, one for regular users and one for Admin users. The login redirect path is currently set in Start.cs when setting up UseCookieAuthentication. I am pretty sure I could parse the returnUrl and look for /admin/ in the path, but that seems really hacky and prone to errors (what if there is no returnUrl?). Is there a better way? I see stuff online about using an authentication filter, but not sure if that is workable when using OWIN.
#AntP is right, I had the same problem and I solved using Authentication Filter. (I am also using OWIN Authentication).
In my case I wanted the user to be redirected to a different Login page according to the area.
public class AreaAuthorizationAttribute : FilterAttribute, IAuthenticationFilter
{
string clientRole = "Client"; // can be taken from resource file or config file
string adminRole = "Admin"; // can be taken from resource file or config file
string currentArea = "";
public void OnAuthentication(AuthenticationContext context)
{
var area = context.RouteData.DataTokens["area"];
if (context.HttpContext.User.Identity.IsAuthenticated)
{
if (context.HttpContext.User.IsInRole(clientRole) && !(area.ToString().Equals("Client")))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Client";
}
if (context.HttpContext.User.IsInRole(adminRole) && !(area.ToString().Equals("Admin")))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Admin";
}
}
else
{
if (area.ToString().Equals("Client"))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Client";
} else if (area.ToString().Equals("Admin"))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Admin";
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext context)
{
if (context.Result == null)
{
Debug.WriteLine("Context null");
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Client", controller = "Client", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
if (context.Result is HttpUnauthorizedResult)
{
if ((currentArea.Equals("Client")))
{
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Client", controller = "Client", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
else if(currentArea.Equals("Admin"))
{
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Admin", controller = "Admin", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
}
}
}
Then I used my custom attribute as followes:
[AreaAuthorization]
public class AdminController : Controller //controller in Admin area
{
.... //actions
}
[AreaAuthorization]
public class ClientController : Controller //controller in Client area
{
.... //actions
}
This is my source (for custom authorize attributes): http://www.dotnetfunda.com/articles/show/2935/creating-custom-authentication-filter-in-aspnet-mvc
You could put a constraint in your routing rules. This constraint will check is the user is of type Admin and allow (or reject) the routing.
You need to set a IRouteConstraint class. For example:
public class IsAdmin : IRouteConstraint
{
public IsAdmin()
{
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
bool isAdmin = false;
// TO DO, detect if current user is Admin and return the value
// ...
return isAdmin;
}
}
Then in your routing rules you add that as a constraint
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "home", action = "index", id = UrlParameter.Optional },
constraints: new { controller = new Common.Constraints.IsAdmin() }
);
It is a rather simple approach. I have used to prevent non admin users from accessing anything on the /admin section of my website and it works like a charm.

Unable to reach ASP.NET MVC Web Api endpoint

I am working on an ASP.NET MVC app. I am trying to create a basic API. I created my first Web API controller by right-clicking on Controllers, Add -> Controller... then choosing "Web API 2 Controller - Empty". In the controller code, I have the following:
namespace MyProject.Controllers
{
public class MyApiController : ApiController
{
public IHttpActionResult Get()
{
var results = new[]
{
new { ResultId = 1, ResultName = "Bill" },
new { ResultId = 2, ResultName = "Ted" }
};
return Ok(results);
}
}
}
When I run the app, I enter http://localhost:61549/api/myApi in the browser's address bar. Unfortunately, I get a 404. I'm just trying to create an API endpoint that returns a hard-coded set of JSON objects. I need this to test some client-side JavaScript. What am I doing wrong?
Here are how my routes are registered:
WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
RouteConfig.cs
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 }
);
}
Make sure that you have the WebApiConfig registration being called, possibly in the Global.asax Application_Start() method. Something like:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
You did not add method name at the end of call. Try this one:
http://localhost:61549/api/myapi/get
Try this approach
namespace MyProject.Controllers
{
public class MyApiController : ApiController
{
public IHttpActionResult Get()
{
var results = new List<ResultModel>
{
new ResultModel() {ResultId = 1, ResultName = "Bill"},
new ResultModel() {ResultId = 2, ResultName = "Ted"}
};
return Ok(results);
}
}
public class ResultModel
{
public int ResultId { get; set; }
public string ResultName { get; set; }
}
}
Api: http://localhost:61549/api/MyApi/get
Hope this helps.

How to change routes globally?

I want to switch my views in MVC 3 between two languages - PL and EN. I've created two folders in Views- EN and PL. So after clicking appropriate language link at any site I want my route change from:
routes.MapRoute(
"pl", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "PL", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
to:
routes.MapRoute(
"en", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "EN", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
When I click appropriate link (language switcher) it changes CultureInfo which is persistent to all threads.
_Layout View with switcher:
<ul>
<li>#Html.ActionLink("En", "ChangeCulture", null, new { lang = "en"}, null)</li>
<li>#Html.ActionLink("Pl", "ChangeCulture", null, new { lang = "pl"}, null)</li>
</ul>
and controller (which sets also static variable lang that can be seen in every controller's method and be persistent between requests):
public ActionResult ChangeCulture(string lang)
{
PLController.lang = lang;
CultureSettings setCulture = new CultureSettings();
setCulture.InitializeCulture(lang);
cookie.Value = CultureInfo.CurrentCulture.Name;
this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);
return View("Index");
}
InitializeCulture method is overriden from Page class as follows:
public class CultureSettings : Page{
public void InitializeCulture(string culture)
{
String selectedLanguage;
if(culture == null)
{
selectedLanguage = "pl";
}
else
{
selectedLanguage = culture;
}
UICulture = selectedLanguage;
Culture = selectedLanguage;
Thread.CurrentThread.CurrentCulture =
CultureInfo.CreateSpecificCulture(selectedLanguage);
Thread.CurrentThread.CurrentUICulture = new
CultureInfo(selectedLanguage);
base.InitializeCulture();
}
}
It sets CultureInfo properly. Now I want (according to current CultureInfo) switch routes for every navigation links and change route pattern from mysite.com/PL/{controller}/{action} to mysite.com/EN/{controller}/{action}.
Does anyone has any ideas or maybe better approach for this problem? But condition is that address must be looking like this mysite.com/EN or mysite.com/PL - not different (i.e. en.mysite.com)
The first thing that you must decide is where to store the current user language. There are different possibilities:
part of every url
cookie
session
IMHO for SEO purposes it's best to have it as part of the url.
So I would recommend writing a custom route which will parse the language from the url and set the current thread culture:
public class LocalizedRoute : Route
{
public LocalizedRoute()
: base(
"{lang}/{controller}/{action}/{id}",
new RouteValueDictionary(new
{
lang = "en-US",
controller = "home",
action = "index",
id = UrlParameter.Optional
}),
new RouteValueDictionary(new
{
lang = #"[a-z]{2}-[a-z]{2}"
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var lang = rd.Values["lang"] as string;
if (string.IsNullOrEmpty(lang))
{
// pick a default culture
lang = "en-US";
}
var culture = new CultureInfo(lang);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
return rd;
}
}
We could now register this custom route in Global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("Default", new LocalizedRoute());
}
Alright, now let's have a model:
public class MyViewModel
{
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime Date { get; set; }
}
A controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
Date = DateTime.Now
});
}
public ActionResult Test()
{
return Content(DateTime.Now.ToLongDateString());
}
}
And a view:
#model MyViewModel
#Html.DisplayFor(x => x.Date)
<ul>
<li>#Html.ActionLink("switch to fr-FR", "index", new { lang = "fr-FR" })</li>
<li>#Html.ActionLink("switch to de-DE", "index", new { lang = "de-DE" })</li>
<li>#Html.ActionLink("switch to en-US", "index", new { lang = "en-US" })</li>
</ul>
#Html.ActionLink("Test culture", "test")
Now when you click on the links we are changing the language and this language is now part of the routes. Notice how once you have chosen the language this language is being persisted in the routes for the test link.
Scott Hanselman also wrote a nice blog post on localization and globalization in ASP.NET that's worth checking out.
This solution will not solve this issue:
english: /en/home
french: /fr/accueil
english: /en/contactus
french: /fr/contacteznous
It will just do
/en/home
/fr/home
You should also localize the routes in a dictionary and do a lookup.

Resources