URL Amigável e Dinâmica no MVC

Disponível também em inglês

URL dinâmica é uma grande funcionalidade no MVC. URL amigável é ainda melhor. A solução desse artigo acredito que seja a melhor maneira de trabalhar com URLs amigáveis.

Vamos definir algumas premissas:

1 – As URLs precisam ser guardadas em um repositório. Isso significa que eu quero poder alterar e criar mais URL quando quiser de forma fácil;
2 – Uma ou mais URL pode apontar para o mesmo Controller/Action. Quer dizer que eu posso criar apelido para as URLs;
3 – Se uma URL não existir no meu repositório, tente resolver a URL usando o comportamento padrão do MVC Controller/Action. O comportamento padrão ainda funcionará;
4 – A URL pode ou não conter um ID ao final da URL. Significa que a última parte da URL pode ser um ID número inteiro;

 
URL Amigável e Dinâmica no MVC
 

Primeiro de tudo, o MVC não tem uma funcionalidade integrada e já pronta para URL dinâmica e amigável, nós temos que implementar.

Para a nossa solução precisaremos de:
1 – Um projeto MVC ;
2 – Uma classe para controlar as requisições de rotas;
3 – Um repositório de rotas;
4 – Controllers e views;

PS: Eu não irei usar um banco de dados para guardar as URLs mas irei usar o design patter de Repository e dependency resolver para resolver a dependência. Assim futuramente podemos mudar apenas uma parte do código para colocar o banco de dados sem afetar todo o resto.

A classe que resolver e identifica uma URL.

Handlers/UrlHandler.cs

public sealed class UrlHandler
    {
        public static UrlRouteData GetRoute(string url)
        {
            url = url ?? "/";
            url = url == "/" ? "" : url;
            url = url.ToLower();

            UrlRouteData urlRoute = null;

            using (var repository = DependencyResolver.Current.GetService<IRouteRepository>())
            {
                var routes = repository.Find(url);
                var route = routes.FirstOrDefault();
                if (route != null)
                {
                    route.Id = GetIdFromUrl(url);
                    urlRoute = route;
                    urlRoute.Success = true;
                }
                else
                {
                    route = GetControllerActionFromUrl(url);
                    urlRoute = route;
                    urlRoute.Success = false;
                }
            }

            return urlRoute;
        }

        private static RouteData GetControllerActionFromUrl(string url)
        {
            var route = new RouteData();

            if (!string.IsNullOrEmpty(url))
            {
                var segmments = url.Split('/');
                if (segmments.Length == 1)
                {
                    route.Id = GetIdFromUrl(url);
                    route.Controller = segmments[0];
                    route.Action = route.Id == 0? (segmments.Length == 2? segmments[1] : route.Action) : route.Action;
                }
            }

            return route;
        }

        private static long GetIdFromUrl(string url)
        {
            if (!string.IsNullOrEmpty(url))
            {
                var segmments = url.Split('/');
                if (segmments.Length == 1)
                {
                    var lastSegment = segmments[segmments.Length - 1];
                    long id = 0;
                    long.TryParse(lastSegment, out id);

                    return id;
                }
            }

            return 0;
        }
    }

Classe de controle de rotas que recebe todas as requisições.

Handlers/UrlRouteHandler.cs

public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            var routeData = requestContext.RouteData.Values;
            var url = routeData["urlRouteHandler"] as string;
            var route = UrlHandler.GetRoute(url);

            routeData["url"] = route.Url;
            routeData["controller"] = route.Controller;
            routeData["action"] = route.Action;
            routeData["id"] = route.Id;
            routeData["urlRouteHandler"] = route;

            return new MvcHandler(requestContext);
        }

A configuração das rotas.

App_Start/RouteConfig.cs

public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "IUrlRouteHandler",
                "{*urlRouteHandler}").RouteHandler = new UrlRouteHandler();
        }
    }

Classes de repositório que serão armazenadas as URL amigável.

Repository/IRouteRepository.cs

public interface IRouteRepository : IDisposable
    {
        IEnumerable<RouteData> Find(string url);
    }

Repository/StaticRouteRepository.cs

public class StaticRouteRepository : IRouteRepository
    {
        public void Dispose()
        {

        }

        public IEnumerable<RouteData> Find(string url)
        {
            var routes = new List<RouteData>();
            routes.Add(new RouteData()
            {
                RoouteId = Guid.NewGuid(),
                Url = "how-to-write-file-using-csharp",
                Controller = "Articles",
                Action = "Index"
            });
            routes.Add(new RouteData()
            {
                RoouteId = Guid.NewGuid(),
                Url = "help/how-to-use-this-web-site",
                Controller = "Help",
                Action = "Index"
            });

            if (!string.IsNullOrEmpty(url))
            {
                var route = routes.SingleOrDefault(r => r.Url == url);
                if (route == null)
                {
                    route = routes.FirstOrDefault(r => url.Contains(r.Url)) ?? routes.FirstOrDefault(r => r.Url.Contains(url));
                }

                if (route != null)
                {
                    var newRoutes = new List<RouteData>();
                    newRoutes.Add(route);

                    return newRoutes;
                }
            }

            return new List<RouteData>();
        }
    }

Eu criei duas URL. Uma URL irá apontar para o HelpController e a outra para ArticlesController.

Dependência de injeção do repositório. Eu uso Ninject para configurar isso.

App_Start/NinjectWebCommon.cs

private static void RegisterServices(IKernel kernel)
{
    kernel.Bind<Repository.IRouteRepository>().To<Repository.StaticRouteRepository>();
}

Bom é isso. Esperto que tenha ajudado.

Abaixo encontrará alguns links úteis.

URL amigável e dinâmica no MVC

URL amigável: Perguntas, sugestões ou críticas são bem vindas. Boa sorte!

Faça download completo do código fonte no github.

Veja um demo online dessa aplicação no codefinal.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 16 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 14 anos somente para o mercado de seguros.