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.

Leave a Comment