Routing in ASP.NET MVC, Showing Username in Url

Routing in ASP.NET MVC, showing username in URL

On its own, your routing will not work because if the url was .../Product meaning that you wanted to navigate to the Index() method of ProductController, it would match your first route (and assume "Product" is the username. You need to add a route constraint to your roue definitions that returns true if the username is valid and false if not (in which case it will try the following routes to find a match).

Assuming you have a UserController with the following methods

// match http://..../Bryan
public ActionResult Index(string username)
{
// displays the home page for a user
}

// match http://..../Bryan/Photos
public ActionResult Photos(string username)
{
// displays a users photos
}

Then you route definitions need to be

public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "User",
url: "{username}",
defaults: new { controller = "User", action = "Index" },
constraints: new { username = new UserNameConstraint() }
);
routes.MapRoute(
name: "UserPhotos",
url: "{username}/Photos",
defaults: new { controller = "User", action = "Photos" },
constraints: new { username = new UserNameConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Test", action = "Index", id = UrlParameter.Optional }
);
}

public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> users = new List<string>() { "Bryan", "Stephen" };
// Get the username from the url
var username = values["username"].ToString().ToLower();
// Check for a match (assumes case insensitive)
return users.Any(x => x.ToLower() == username);
}
}
}

If the url is .../Bryan, it will match the User route and you will execute the Index() method in UserController (and the value of username will be "Bryan")

If the url is .../Stephen/Photos, it will match the UserPhotos route and you will execute the Photos() method in UserController (and the value of username will be "Stephen")

If the url is .../Product/Details/4, then the route constraint will return false for the first 2 route definitions and you will execute the Details() method of ProductController

If the url is .../Peter or .../Peter/Photos and there is no user with username = "Peter" then it will return 404 Not Found

Note that the the sample code above hard codes the users, but in reality you will call a service that returns a collection containing the valid user names. To avoid hitting the database each request, you should consider using MemoryCache to cache the collection. The code would first check if it exists, and if not populate it, then check if the collection contains the username. You would also need to ensure that the cache was invalidated if a new user was added.

How to change route to username after logged in?

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.

MVC Application Username in route upon login

I believe it should be something like this:

routes.MapRoute(
name: "UserNameRoute",
url: "Dashboard/User/{username}",
defaults: new { username = "" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

I chose not to put the {controller} and {action} placeholders in the template as that would make it equal to the default route template. You could of course use just the action template if that is what you need:

routes.MapRoute(
name: "UserNameRoute",
url: "Dashboard/{action}/{username}",
defaults: new { action = "User", username = "" }
);

Also note I put it before the default route because it is more specific. Otherwise the default route would catch every request.

Authorization is something you can't do here however. You will need to do that by e.g. creating a custom AuthorizeAttribute subclass that checks the username matches the logged in user.

How to make routing in asp.net mvc domainname/username so each user has his username like facebook does

You just use the username part as the {id} element, and have a default {action}.

ASP.Net MVC Route to Username

Here's what you want to do, first define your route map:

routes.MapRoute(
"Users",
"{username}",
new { controller = "User", action="index", username=""});

What this allows you to do is to setup the following convention:

  • Controller: User (the UserController type)
  • Action: Index (this is mapped to the Index method of UserController)
  • Username: This is the parameter for the Index method

So when you request the url http://mydomain.com/javier this will be translated to the call for UserController.Index(string username) where username is set to the value of javier.

Now since you're planning on using the MembershipProvider classes, you want to something more like this:

 public ActionResult Index(MembershipUser usr)
{
ViewData["Welcome"] = "Viewing " + usr.UserName;

return View();
}

In order to do this, you will need to use a ModelBinder to do the work of, well, binding from a username to a MembershipUser type. To do this, you will need to create your own ModelBinder type and apply it to the user parameter of the Index method. Your class can look something like this:

public class UserBinder : IModelBinder
{
public ModelBinderResult BindModel(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
var username = request["username"];
MembershipUser user = Membership.GetUser(username);

return new ModelBinderResult(user);
}
}

This allows you to change the declaration of the Index method to be:

public ActionResult Index([ModelBinder(typeof(UserBinder))] 
MembershipUser usr)
{
ViewData["Welcome"] = "Viewing " + usr.Username;
return View();
}

As you can see, we've applied the [ModelBinder(typeof(UserBinder))] attribute to the method's parameter. This means that before your method is called the logic of your UserBinder type will be called so by the time the method gets called you will have a valid instance of your MembershipUser type.



Related Topics



Leave a reply



Submit