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.
Related
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" });
I have a database, need web interface for administration and management (number of transactions, billing, and other administration) and to serve data from database (products) "manually" and API to serve data (products) to other bigger clients. All secured by SSL and https obviously.
I made a asp.net MVC 5 app (business logic and administration) and wanted to implement API (noob in API) for delivering data to users.
Have no idea how to implement security from MVC to API(same database).
The app is small and I can rewrite it. I'm thinking to try with core, but fear that i will be stuck with same problem.
Concrete question: What approach do I take and weather it should be in MVC 5 generation or .core (MVC 6) to be able to use one database for data, users and their authorizations?
(pushing everything true API is something I would like to avoid)
Ok, my project is done. I moved ahead on MVC 5.
(I apologize to you perfectionists, but I don't have the time now to strip unnecessary thing so I dumped whole files as they are :)
1st approach - abandoned
First I tried designing it as is recommended true the internet: .MVC solution, .DB for database and .API solution.
Conclusion - A lot of problems wit authentication and entity framework. At the end I abandoned this approach
2nd and successful approach
Just one solution .MVC
True NuGet installed .net Api, used integrated authorization extended using few tutorials (not single one worked). Note that I use ASP.NET Identity 2.0 Extending Identity Models and Using Integer Keys Instead of Strings and Implementing HTTPS Everywhere in ASP.Net MVC application.
Here are modifications and addons:
App_Start -> IdentityConfig.cs
public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
// *** ADD INT TYPE ARGUMENT TO CONSTRUCTOR CALL:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
// *** PASS CUSTOM APPLICATION USER STORE AS CONSTRUCTOR ARGUMENT:
var manager = new ApplicationUserManager(
new ApplicationUserStore(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserValidator = new UserValidator<ApplicationUser, int>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// other code removed for brevity
manager.UserLockoutEnabledByDefault = Convert.ToBoolean(ConfigurationManager.AppSettings["UserLockoutEnabledByDefault"].ToString());
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(Double.Parse(ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"].ToString()));
manager.MaxFailedAccessAttemptsBeforeLockout = Convert.ToInt32(ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"].ToString());
// Register two factor authentication providers.
// This application uses Phone and Emails as a step of receiving a
// code for verifying the user You can write your own provider and plug in here.
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
//manager.RegisterTwoFactorProvider("PhoneCode",
// new PhoneNumberTokenProvider<ApplicationUser, int>
// {
// MessageFormat = "Your security code is: {0}"
// });
//// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
//manager.RegisterTwoFactorProvider("EmailCode",
// new EmailTokenProvider<ApplicationUser, int>
// {
// Subject = "SecurityCode",
// BodyFormat = "Your security code is {0}"
// });
//manager.EmailService = new EmailService();
//manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser, int>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO BASE:
public class ApplicationRoleManager : RoleManager<ApplicationRole, int>
{
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO CONSTRUCTOR:
public ApplicationRoleManager(IRoleStore<ApplicationRole, int> roleStore)
: base(roleStore)
{
}
// PASS CUSTOM APPLICATION ROLE AS TYPE ARGUMENT:
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(
new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
}
}
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
return Task.FromResult(0);
}
}
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
return Task.FromResult(0);
}
}
//This is useful if you do not want to tear down the database each time you run the application.
//public class ApplicationDbInitializer : DropCreateDatabaseAlways<ApplicationDbContext>
//This example shows you how to create a new database if the Model changes
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
//InitializeIdentityForEF(context); //- Do not Seed - IGOR
//base.Seed(context);
}
//Create User=Admin#Admin.com with password=Admin#123456 in the Admin role
//public static void InitializeIdentityForEF(ApplicationDbContext db)
//{
// var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
// var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
// const string name = "igor#email.mail";
// const string password = "LolLol1";
// const string roleName = "lol";
// //Create Role Admin if it does not exist
// var role = roleManager.FindByName(roleName);
// if (role == null)
// {
// role = new ApplicationRole(roleName);
// var roleresult = roleManager.Create(role);
// }
// var user = userManager.FindByName(name);
// if (user == null)
// {
// user = new ApplicationUser { UserName = name, Email = name };
// var result = userManager.Create(user, password);
// result = userManager.SetLockoutEnabled(user.Id, false);
// }
// // Add user admin to Role Admin if not already added
// var rolesForUser = userManager.GetRoles(user.Id);
// if (!rolesForUser.Contains(role.Name))
// {
// var result = userManager.AddToRole(user.Id, role.Name);
// }
//}
}
public class ApplicationSignInManager : SignInManager<ApplicationUser, int>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) :
base(userManager, authenticationManager)
{ }
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
App_Start -> Startup.Auth.cs
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and role manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
validateInterval: TimeSpan.FromMinutes(2880),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
// Need to add THIS line because we added the third type argument (int) above:
getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication(
// clientId: "",
// clientSecret: "");
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"), //TODO - makni ovo
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
}
//public partial class Startup
//{
// public void ConfigureAuth(IAppBuilder app)
// {
// // Enable the application to use a cookie to store information for the signed in user
// app.UseCookieAuthentication(new CookieAuthenticationOptions
// {
// ExpireTimeSpan = TimeSpan.FromHours(24),
// CookieSecure = CookieSecureOption.Never,
// CookieHttpOnly = false,
// SlidingExpiration = true,
// AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
// LoginPath = new PathString("/Account/Login")
// });
// // Use a cookie to temporarily store information about a user logging in with a third party login provider
// app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// }
//}
App_Start -> WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// TODO: Add any additional configuration code.
// Web API routes
config.MapHttpAttributeRoutes();
//config.Routes.MapHttpRoute(
// name: "getkey",
// routeTemplate: "api/ApiKeys/Get/{term}"
//);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// WebAPI when dealing with JSON & JavaScript!
// Setup json serialization to serialize classes to camel (std. Json format)
var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
// make all web-api requests to be sent over https
config.MessageHandlers.Add(new EnforceHttpsHandler());
}
}
MySysAdmin controller that I use for initial insert and edit of roles and initial user.
public SysAdminController(ApplicationUserManager userManager,
ApplicationRoleManager roleManager)
{
UserManager = userManager;
RoleManager = roleManager;
}
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
set
{
_userManager = value;
}
}
private ApplicationRoleManager _roleManager;
public ApplicationRoleManager RoleManager
{
get
{
return _roleManager ?? HttpContext.GetOwinContext().Get<ApplicationRoleManager>();
}
private set
{
_roleManager = value;
}
}
public ActionResult RoleIndex()
{
return View(RoleManager.Roles);
}
public ActionResult RoleCreate()
{
return View();
}
[HttpPost]
public async Task<ActionResult> RoleCreate(SysAdminVM.RoleViewModel roleViewModel)
{
if (ModelState.IsValid)
{
// Use ApplicationRole, not IdentityRole:
var role = new ApplicationRole(roleViewModel.Name);
var roleresult = await RoleManager.CreateAsync(role);
if (!roleresult.Succeeded)
{
ModelState.AddModelError("", roleresult.Errors.First());
return View();
}
return RedirectToAction("RoleIndex");
}
return View();
}
public async Task<ActionResult> RoleEdit(int id)
{
if (id > 0)
{
var role = await RoleManager.FindByIdAsync(id);
if (role == null)
{
return HttpNotFound();
}
SysAdminVM.RoleViewModel roleModel = new SysAdminVM.RoleViewModel { Id = role.Id, Name = role.Name };
return View(roleModel);
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> RoleEdit([Bind(Include = "Name,Id")] SysAdminVM.RoleViewModel roleModel)
{
if (ModelState.IsValid)
{
var role = await RoleManager.FindByIdAsync(roleModel.Id);
role.Name = roleModel.Name;
await RoleManager.UpdateAsync(role);
return RedirectToAction("RoleIndex");
}
return View();
}
[AllowAnonymous]
public async Task<ActionResult> Initialize()
{
if (db.App.Where(x => x.Name.Contains("Initialize")).FirstOrDefault() == null)
{
await InitRoleCreate();
await InitUser();
db.App.Add(
new App { Name = "Initialize", Val = "true" }
);
db.SaveChanges();
return View();
}
return HttpNotFound();
}
private async Task InitRoleCreate()
{
var model = new List<string>()
{
"SysAdmin",
"Admin",
"User",
};
foreach (var item in model)
{
var role = new ApplicationRole(item);
await RoleManager.CreateAsync(role);
}
}
private async Task InitUser()
{
var user = new ApplicationUser
{
UserName = "HerGiz",
Email = "hergiz#outlook.com",
Name = "Igor Hermanović",
Contact = "098 185 3131",
TwoFactorEnabled = false,
LockoutEnabled = true,
EmailConfirmed = true
};
var adminResult = await UserManager.CreateAsync(user, "W7xtc2ywfb");
await UserManager.AddToRolesAsync(user.Id, "SysAdmin");
}
}
Entire API part that I need - controller and out of the box login (that is berried somewhere):
[Authorize]
public class ApiKeysController : ApiController
{
[Authorize]
[Route("api/getkey/{term}")]
public ShowFullKeyVM Get(string term)
{
if (User.Identity.IsAuthenticated == true)
{
if (!string.IsNullOrWhiteSpace(term) && (term.Length == 15 || term.Length == 16))
{
var lKey = new LKey();
var vm = lKey.Search(term);
if (vm != null)
{
return vm;
}
}
return new ShowFullKeyVM() { Error = "IMEI either is not valid :(", SearchIMEI = term };
}
return new ShowFullKeyVM() { Error = "Not Authenticated!!!", SearchIMEI = term };
}
}
Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
MvcHandler.DisableMvcResponseHeader = true;
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
App_Start.AutoMapperConfig.DefineMaps();
ModelBinders.Binders.Add(typeof(decimal), new Extensions.DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new Extensions.DecimalModelBinder());
}
Web.config
<appSettings>
<add key="UserLockoutEnabledByDefault" value="true" />
<add key="DefaultAccountLockoutTimeSpan" value="30" />
<add key="MaxFailedAccessAttemptsBeforeLockout" value="4" />
</appSettings>
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 }
);
I have following patterns
/invitation/mission/busstop -- return the list of busstops
/invitation/mission/busstop/id -- return a specific busstop
/invitation/mission/driver -- return the list of drivers
/invitation/mission/driver/id -- return a specific driver
/invitation/mission/driver/city/model/limit -- query driver accoring to city, model and age limit
...
/invitation/questionair -- return the list of questionairs
/invitation/questionair/id -- return a specific questionair
/invitation/questionair/create -- create a new questionair
/invitation/questionair/update/id -- update a questionair
...
I expect the 'invitation' to be controller, and the rest to be action. Each of the above url should corresponds to a dedicated view page.
Can anyone help me to design the routes?
=====================================================================
I update the patterns, and add my expectation at the end of each url. Any suggest on the url patterns?
Here is the answer to keep your controller simple and still having good url patterns:
Controller:
public class InvitationController : Controller
{
public ActionResult GetAllBusStops()
{
//Logic to show all bus stops
//return bus stops
return View();
}
public ActionResult GetBusStopById(string id) //Assumed your id to be a string
{
//Logic to get specific bus stop
//return bus stop
return View();
}
public ActionResult GetAllDrivers()
{
//Logic for driver list
//return driver list
return View();
}
public ActionResult GetDriverById(int id) //Assumed your id to be an integer
{
//Logic to get specific driver
//return driver
return View();
}
public ActionResult GetDriver(string city, string model,int limit) //Assumed datatypes
{
//Logic to get specific driver with this criteria
//return driver
return View();
}
public ActionResult GetAllQuestionairs()
{
//Logic for questionair list
//return the list
return View();
}
public ActionResult GetQuestionairById(int id) //Assumed your id to be an integer
{
//Logic to get specific questionair
//return it
return View();
}
public ActionResult CreateQuestionair(QuestionairCreateModel model)
{
//logic to create questionair
return View();
}
public ActionResult GetQuestionairById(int id) //Assumed your id to be an integer
{
//Logic to get specific questionair
//return it
return View();
}
public ActionResult UpdateQuestionairById(int id) //Assumed your id to be an integer
{
//Logic to update specific questionair
//return it
return View();
}
}
Now go to your App_Start Folder, Open RouteConfig.cs, start adding the REST urls in the routes as below:
routes.MapRoute(
"List Bus Stops", // Route name
"invitation/mission/busstop", // No parameters for Getting bus stop list
new { controller = "Invitation", action = "GetAllBusStops"}, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get Bus stop by id", // Route name
"invitation/mission/busstop/{id}", // URL with parameters
new { controller = "Invitation", action = "GetBusStopById" }, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get All Drivers", // Route name
"invitation/mission/driver", // No parameters for Getting driver list
new { controller = "Invitation", action = "GetAllDrivers"}, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get Driver by id", // Route name
"invitation/mission/driver/{id}", // URL with parameters
new { controller = "Invitation", action = "GetDriverById" }, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get driver for city, model, limit", // Route name
"invitation/mission/driver/{city}}/{model}/{limit}", // URL with parameters
new { controller = "invitation", action = "GetDriver"}, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get All Questionairs", // Route name
"invitation/questionair", // No parameters for Getting questionair list
new { controller = "Invitation", action = "GetAllQuestionairs"}, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Get questionair by id", // Route name
"invitation/questionair/{id}", // URL with parameters
new { controller = "Invitation", action = "GetQuestionairById" }, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("GET") }
);
routes.MapRoute(
"Create New Questionair, // Route name
"invitation/questionair/create", // URL with parameters
new { controller = "invitation", action = "CreateQuestionair" }, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("POST") }
);
routes.MapRoute(
"Update Questionair, // Route name
"invitation/questionair/update/{id}", // URL with parameters
new { controller = "invitation", action = "UpdateQuestionairById" }, // Parameter defaults
new { httpMethod = new HttpMethodConstraint("POST") }
);
Many things are assumed like the datatypes and model names. You can figure out how it works from this....
Add the route in the global.asax
routes.MapRoute(
"Default",
"{controller}/{id}",
new { controller = "invitation", action = "Index" }
);
Then in your controller use something like:
public class InvitationController : Controller
{
public ActionResult Index(string id)
{
switch(id.ToLower())
{
case "mission/busstop/list":
return View("busstop/list");
case "mission/driver/query":
return View("driver/query");
}
//Return default view
return View();
}
}
The other answers are valid, but I think there is a much easier, cleaner, and more maintainable way of defining your routes: use an attribute-based route mapper.
The easiest one I've found to use is the RiaLibrary.Web route mapper. All you have to do is add an attribute on each method you want to have a route for - then specify your route details.
To get set up, you must follow a few steps outlined on the RiaLibrary.Web page. After completing those steps, you can change any Controller action to look like this:
[Url("Articles/{id}")]
public ActionResult Details(int id)
{
// process
return View();
}
If you have any optional parameters, declare them as {param?} in the route string and then as int? param in your method declaration.
That's it! You can also include some other parameters to specify whether only certain types of HTTP calls match this route, to define constraints on parameters, and to fix the order in which this route is matched.
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.