IRouteHandler no ASP.NET MVC

Neste post vou comentar um pouco sobre a interface IRouteHandler do ASP.NET MVC e como criar uma classe que a implemente. O objetivo é criar uma classe customizada para manipular a rota da aplicação. Esta é uma das inúmeras formas de estender a sua aplicação. Mas antes, vou explicar um pouco sobre como funciona uma requisição numa aplicação ASP.NET MVC.

Toda requisição recebida no ASP.NET MVC passa por um objeto chamado UrlRoutingModule, que é um módulo HTTP. Este módulo analisa a requisição e seleciona qual rota será utilizada. Esta rota é um objeto de uma classe que herda de RouteBase, e é geralmente uma instância da classe Route.

Uma vez obtido o objeto Route, o UrlRoutingModule obtém o objeto que implementa a interface IRouteHandler, que está associada na classe da rota. Por padrão, em uma aplicação MVC, será uma instância de MvcRouteHandler. A instância de IRouteHandler cria o objeto IHttpHandler, que por padrão será a classe MvcHandler. Por fim, o MvcHandler selecionará o controller que irá manipular a requisição.

Como exemplo, vou utilizar o seguinte cenário.  Considere que o seu site possua uma área restrita à determinados países. Por exemplo, determinado país não poderá acessar determinada área do seu site.

O primeiro passo será criar uma classe que implemente a interface IRouteHandler. Esta interface possui um único método chamado GetHttpHandler, que recebe um RequestContext como parâmetro e retorna IHttpHandler. A classe que será criada chamará RestricaoPaiRouteHandler. Ela receberá em seu construtor uma lista de códigos de países que não estão autorizados à acessar o meu site. E no método GetHttpHandler será criada uma instância de uma classe que implementa IHttpHandler, que também será criada posteriormente.

using System.Collections.Generic;
using System.Web;
using System.Web.Routing;

namespace CustomRoute.App_Start
{
    public class RestricaoPaisRouteHandler : IRouteHandler
    {
        #region Campos

        private List paises;

        #endregion

        #region Construtor

        public RestricaoPaisRouteHandler(List paises)
        {
            this.paises = paises;
        }

        #endregion

        #region Métodos

        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            BloqueioIpHandler handler = new BloqueioIpHandler(this.paises, requestContext);

            return handler;
        }

        #endregion
    }
}

Em seguida, será criada a classe BloqueioIpHandler herdando de MvcHandler. A classe MvcHandler já implementa IHttpHandler. Neste caso iremos apenas sobrescrever a sua implementação. Será sobrescrito o método BeginProcessRequest com a seguinte lógica. Será chamado um método ObterCodigoPais, que através do IP irá retornar a sigla do país que está realizando o acesso ao site. E se o país estiver na lista de países bloqueados será criada uma exceção.

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Xml.Linq;

namespace CustomRoute.App_Start
{
    public class BloqueioIpHandler : MvcHandler
    {
        #region Campos

        private List paises;

        #endregion

        #region Construtor

        public BloqueioIpHandler(List paises, RequestContext requestContext)
            : base(requestContext)
        {
            this.paises = paises;
        }

        #endregion

        #region Métodos

        private string ObterCodigoPais(string ip)
        {
            string query = string.Format("http://api.hostip.info/?ip={0}", ip);

            XDocument doc = XDocument.Load(query);

            XNamespace defaultNamespace = doc.Root.GetDefaultNamespace();
            XNamespace xNamespace = doc.Root.GetNamespaceOfPrefix("gml");

            string pais = doc.Root.Element(xNamespace + "featureMember")
                .Element(defaultNamespace + "Hostip").Element(defaultNamespace + "countryAbbrev").Value;

            return pais; 
        }

        protected override IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state)
        {
            string pais = ObterCodigoPais(httpContext.Request.UserHostAddress);

            if (this.paises.Contains(pais))
                httpContext.AddError(new Exception("Desculpe! Você não pode acessar esta página a partir do seu país."));

            return base.BeginProcessRequest(httpContext, callback, state);
        }

        #endregion
    }
}

Após a criação das classes, é necessário registrar uma nova rota e definir a classe criada para manipular a mesma. Para isto altere a classe RouteConfig localizada na parta App_Start do projeto. Será definido uma nova rota, além da padrão, e nela será definida a propriedade RouteHandler.

using CustomRoute.App_Start;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

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

            routes.MapRoute(
                name: "Restrito",
                url: "Restrito/{action}",
                defaults: new { controller = "Restrito", action = "Index" }
            ).RouteHandler = new RestricaoPaisRouteHandler(new List() { "XX" });

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

Agora ao acessar o controller Restrito será exibida uma mensagem de erro, informando que o país não está autorizado. No exemplo foi definido a sigla XX pois é a sigla que será retornada quando informado um IP local.

Quero ressalvar que este é um simples exemplo de como criar classe que faça um tratamento customizado da rota. Isto possibilita fazer qualquer tratamento da requisição. Há também outras formas de realizar o bloqueio por região, como por exemplo fazer o controle no próprio controller. Para fazer o exemplo foi utilizado o Visual Studio 2015 e o MVC 5.2.

O código fonte de exemplo está disponível no GitHub. Para acessar clique aqui.