Fastest template engine for Node.js

I’ve searching for a good template engine with good syntax and features like Razor for .NET. I’ve tried several different template engines, so I’ve learn to create my own.

That was the result: jsengine

Here is the benchmark:
fastest

Anúncios

Autenticação e Autorização Customizada no ASP.NET/MVC

Introdução

Neste post vou apresentar uma implementação efetiva para autenticação e autorização customizada de usuários em aplicações ASP.NET/MVC.

Nesta implementação, será utilizado FormsAuthentication, MemoryCache, filtros de Autorização MVC e EntityFramework Code First.

FormsAuthentication

Apesar de prestes a se tornar obsoleto e sendo substituído pelo IdentityFramework, cumpre muito bem o seu papel, que é o de autenticar usuários. Aguardarei a versão final do ASP.NET vNext para conferir qual será o padrão utilizado antes de pensar em trocar de mecanismo de autenticação. Duas outras implementações interessantes: AppHarbor e MembershipReboot.

MemoryCache

Será utilizado para armazenar informações de um usuário autenticado e suas permissões, evitando consultas SQL desnecessárias a cada request, já que esses dados mudam muito pouco. Utilizaremos como chave o nome de usuário que está armazenado no cookie de autenticação. Cada entrada no cache terá uma duração determinada, ficando a critério do programador. Por exemplo, 60 ou 120 segundos são suficientes. Na implementação, forneço um meio para que o usuário seja removido do cache imediatamente, reagindo à alterações feitas em suas permissões.

EntityFramework & CodeFirst

Entity Framework é o ORM (Mapeador Objeto Relacional) oficial da Microsoft. Está em pleno desenvolvimento (atualmente na versão 6.1.1 e versão 7 em sairá em breve) e faz parte dos templates de aplicações Web no Visual Studio desde a versão 2012. O modo CodeFirst, introduzido na versão 4.1, traz a possibilidade de utilizarmos apenas código para expressar entidades e relacionamentos de forma fluente e natural, e o mais importante: elimina-se a dependência do Visual Studio para modelar classes e gerar código. Além disso, com o CodeFirst, podemos fazer ajustes finos no mapeamento com o banco de dados, sem o risco de perder as nossas customizações pois não há um gerador de código envolvido no processo. Outra vantagem é a facilidade de se trabalhar em equipes (utilizando Git ou Team Foundation Server), pois podemos separar as entidades em classes com arquivos separados. Na hora das alterações, só fazemos checkin do código que foi alterado, evitando conflitos como os que ocorrem quando vários programadores alteram o mesmo EDMX. Por último, é uma opção muito atrativa para desenvolvedores de outras linguagens, como Ruby (Ruby On Rails). O recurso de Migrations, também presente no Ruby On Rails, utiliza apenas código para expressar as migrações de banco de dados. Veja uma pergunta sobre o assunto respondida no StackOverflow.

Filtros e atributos do MVC

Filtros em forma de atributos no MVC são uma excelente forma de escrever código reaproveitável. Existem diversos filtros disponíveis no Framework, e todos podem ser customizados, inclusive podemos criar nossos próprios filtros. O filtro [Authorize] será utilizado para, de forma declarativa, requisitar autorização para uma determinada Action ou todas as Actions de um controller.

Implementação

Para começar, segue um diagrama de fluxo para facilitar o entendimento de todo o processo:
formsauth (1)

Autenticação

Autenticação é o processo de identificação do usuário, e denota se um ticket de autenticação foi previamente emitido ou não, contendo um identificador para o usuário (geralmente um Id ou Email) independente de seus papéis/permissões. O processo de identificação do usuário pelo FormsAuthentication envolve a existência e validação do cookie de autenticação. O cookie, quando criado, é encriptado usando uma chave de validação e só pode ser descriptografado com a mesma chave. Essa chave deve estar configurada no arquivo web.config da aplicação. Caso sua aplicação não possua uma chave específica, será utilizada a chave global que é única para cada servidor. Caso você queira utilizar o mesmo cookie de autenticação em várias aplicações em servidores diferentes, você deve especificar a mesma chave para todas as aplicações em que deseja compartilhar a autenticação.

<system.web>
  <compilation debug="true" targetFramework="4.5.1"/>
  <httpRuntime targetFramework="4.5.1"/>
  <authentication mode="Forms">
    <forms name="logincentral-cookie" path="/" loginUrl="/session/new" defaultUrl="/session/" timeout="1000" cookieless="UseCookies"
        enableCrossAppRedirects="true" protection="All" requireSSL="false"/>
  </authentication>

<!--http://aspnetresources.com/tools/machinekey-->
  <machineKey validationKey="0E981722AC74FC2CE410DA15439EF5B1279B293772511216953A5E2BF0FFBF1F645AA131A5E9F27F60CA2F1A96C93932BD6FB5BC9C86EFEDD7F7DEB392323AD9" decryptionKey="7BCF9BAEB6FF6B52DE6F95555EDB84A4BCE7D7EB08B30DCC97B513FF1CF9FA7C" validation="SHA1" decryption="AES" />
</system.web>

Dentro da Pipeline do ASP.NET, o FormsAuthenticationModule utiliza o evento Application_AuthenticateRequest para cumprir o seu papel. Nesse evento, o cookie de autenticação, caso tenha sido enviado pelo browser durante a requisição HTTP, é inspecionado e validado. Se tudo der certo, o usuário da aplicação (Request.User) que até então era um usuário anônimo, é substituído com uma nova instância de GenericPrincipal, contendo uma instância FormsIdentity.

Lembrando que esse processo ocorre para todas as requisições recebidas pela aplicação, incluindo requisições para imagens, arquivos estáticos, etc.

Autorização

Autorização é o processo de permitir ou negar o acesso a um recurso ou operação do sistema, após o usuário estar autenticado/identificado. Podemos dizer que o primeiro passo do processo de autorização é a autenticação/identificação.

Aproveitando a infraestrutura já existente no ASP.NET, as permissões serão atribuídas através de “Roles” (papéis). Podemos considerar um Role como um grupo ao qual o usuário pertence, e quando precisarmos restrigir o acesso a um recurso, faremos isso através de grupos.

Como estamos lidando com autenticação e autorização customizadas, devemos atribuir e atualizar os Roles para o usuário previamente autenticado. O evento correto para isso dentro da pipeline do ASP.NET é o Application_PostAuthenticateRequest, ou seja, já temos o usuário identificado e agora vamos modificar suas permissões.

Nesse evento, devemos consultar o banco de dados para verificar quais grupos (“Roles”) foram atribuídos ao usuário e substituir o Request.User com uma nova instância já com as informações atualizadas. A lógica a ser seguida deve obedecer ao código abaixo.

        private static void OnPostAuthenticateRequest(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;

            var context = application.Context;
            var user = context.User;
            var request = context.Request;
            var response = context.Response;

            if (user == null || string.IsNullOrEmpty(user.Identity.AuthenticationType))
                return;

            var formsIdentity = user.Identity as FormsIdentity;
            if (formsIdentity == null)
                return;

            if (!user.Identity.IsAuthenticated)
            {
                //se o usuário não estiver autenticado,
                //não é necessário validar no banco de dados
                return;
            }

            try
            {
                //recupera informações do usuário no banco de dados
                var usuario = GetUserFromDb(user.Identity.Name);

                if (usuario == null || !usuario.Status)
                {
                    //faz o logout se o usuário foi excluído ou desativado
                    FormsAuthentication.SignOut();

                    if (!Utilities.IsAjaxRequest(request))
                    {
                        //redireciona o usuário para a página de login (web.config)
                        FormsAuthentication.RedirectToLoginPage("appName=" + WebConfigurationManager.AppSettings["appName"]);
                    }
                    else
                    {
                        //se for uma requisição ajax, retorna o status 401 ou 403
                        response.StatusCode = 401; //403 ?
                        response.StatusDescription = "Usuário desativado!";
                    }
                    return;
                }

                //preenche os "Roles"
                var roles = GetRolesForUser(user.Identity.Name);

                //troca o generic principal com a nova instância
                var customPrincipal = new GenericPrincipal(formsIdentity, roles);

                Thread.CurrentPrincipal = context.User = customPrincipal;
            }
            catch (Exception ex)
            {
                //logar exception
                Trace.TraceError("Erro: {0}", ex);
                throw;
            }
        }

Como esse evento ocorre para todos os requests e as permissões estão armazenadas em banco de dados, devemos aplicar uma otimização, gravando em cache essas informações, reduzindo consultas desnecessárias ao banco de dados.

O código abaixo mostra a implementação dos métodos que faltaram no código acima:

  static User GetUserFromDb(string login)
  {
      //Verifica se usuário está no cache, e retorna.
      //Senão, vai ao banco de dados e grava no cache
      return CacheApi.GetUser(login) ?? DbApi.GetUser(login);
  }

  static string[] GetRolesForUser(string login)
  {
      return CacheApi.GetRolesForUser(login);
  }

A implementação do Cache segue da seguinte forma:

   public static class CacheApi
    {
        private static readonly object Locker = new object();
        private static readonly int SecondsCache;
        private const string CACHE_KEY = "keepUserInCache";
        private const int DefaultCacheExpiration = 60;

        static CacheApi()
        {
            if (!WebConfigurationManager.AppSettings.AllKeys.Contains(CACHE_KEY)) return;
            if (int.TryParse(WebConfigurationManager.AppSettings[CACHE_KEY], out SecondsCache))
            {
                return;
            }

            SecondsCache = DefaultCacheExpiration;
        }

        public static User GetUser(string userName)
        {
            var cacheEntry = MemoryCache.Default.Get(userName) as CacheEntry;
            return cacheEntry != null
                ? cacheEntry.User
                : null;
        }

        public static string[] GetRolesForUser(string userName)
        {
            var cacheEntry = MemoryCache.Default.Get(userName) as CacheEntry;
            return cacheEntry != null
                ? cacheEntry.Roles
                : Enumerable.Empty<string>().ToArray();
        }


        internal static void UpdateCache(string userName, User user, string[] roles)
        {
            lock (Locker)
            {
                var cacheEntry = MemoryCache.Default.Get(userName) as CacheEntry;
                if (cacheEntry != null)
                {
                    MemoryCache.Default.Remove(userName);
                }
                if (user != null)
                {
                    MemoryCache.Default.Add(userName, new CacheEntry(user, roles), new CacheItemPolicy
                    {
                        AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(SecondsCache),
                        RemovedCallback = RemovedCallback,
                        //SlidingExpiration = TimeSpan.FromMinutes(5),
                        Priority = CacheItemPriority.NotRemovable
                    });
                }
            }
        }

        private static void RemovedCallback(CacheEntryRemovedArguments arguments)
        {
            Trace.TraceInformation("Usuário '{0}' removido do cache de autenticação", arguments.CacheItem);
        }

        /// <summary>
        /// Ao alterar o usuário, limpar do cache para atualizar do banco de dados na próxima chamada
        /// </summary>
        /// <param name="principal"></param>
        public static void ClearUserFromCache(this IPrincipal principal)
        {
            if (principal != null && principal.Identity != null)
            {
                var userName = principal.Identity.Name;
                Trace.TraceInformation("Removendo usuário do cache devido a alterações");
                UpdateCache(userName, null, null);
            }

        }
    }

    ///Representa uma entrada no Cache
    public class CacheEntry
    {
        public User User { get; private set; }
        public string[] Roles { get; private set; }

        public CacheEntry(User user, string[] roles)
        {
            User = user;
            Roles = roles ?? Enumerable.Empty<string>().ToArray();
        }

        public override string ToString()
        {
            return string.Format("{0} - {1}", User != null ? User.ToString() : "-1", Roles != null ? Roles.Count() : 0);
        }
    }

Para as consultas no banco de dados, a classe DbApi foi implementada como segue:

    public static class DbApi
    {
        public static User GetUser(string userName)
        {
            Trace.TraceInformation("Verificando se usuário {0} existe", userName);
            using (var ctx = new LoginCentralContext())
            {
                var user = ctx.Users.FirstOrDefault(x => x.Login.Equals(userName, StringComparison.OrdinalIgnoreCase));

                if (user != null)
                {
                    CacheApi.UpdateCache(userName, user, GetRolesForUser(userName));
                }
                else
                {
                    CacheApi.UpdateCache(userName, null, null);
                }


                Trace.TraceInformation("Usuário {0} existe = {1}", userName, user != null);
                return user;
            }
        }

        static string[] GetRolesForUser(string userName)
        {
            using (var ctx = new LoginCentralContext())
            {
                var roles = new List<string>();

                var user = ctx.Users
                    .Include(x => x.UserRoles)
                    .Include(x => x.UserRoles.Select(s => s.Role))
                    .FirstOrDefault(x => x.Login.Equals(userName, StringComparison.OrdinalIgnoreCase));

                if (user != null)
                {
                    foreach (var userRole in user.UserRoles.Where(x => x.Role.Status))
                    {
                        roles.Add(userRole.Role.RoleName);
                    }
                }

                return roles.ToArray();
            }
        }
    }

Para entendermos a estrutura do banco de dados e os relacionamentos, segue o Code First model do Entity Framework:

[Table("Roles")]
public class Role
{
    public Role()
    {
        UserRoles = new HashSet<UserRole>();
    }

    [Key]
    public int Id { get; set; }
    public string RoleName { get; set; }
    public bool Status { get; set; }
    public ICollection<UserRole> UserRoles { get; set; }
}

[Table("Users")]
public class User
{
    [Key]
    public long Id { get; set; }

    [Required, StringLength(100)]
    public string Login { get; set; }

    [Required, StringLength(8)]
    public string Salt { get; set; }

    [Required, StringLength(44)]
    public string Hash { get; set; }

    public bool Status { get; set; }

    public ICollection<UserRole> UserRoles { get; set; }

    public override string ToString()
    {
        return string.Format("{0} - {1}", Id, Login);
    }
}

[Table("Users_Roles")]
public class UserRole
{
    [Key]
    public long Id { get; set; }

    [ForeignKey("UserId")]
    public User User { get; set; }

    [ForeignKey("RoleId")]
    public Role Role { get; set; }

    [Required]
    public long UserId { get; set; }

    [Required]
    public int RoleId { get; set; }
}

public class LoginCentralContext : DbContext
{
    static LoginCentralContext()
    {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<LoginCentralContext, Configuration>());
    }

    public static void Intialize()
    {
        using (var ctx = new LoginCentralContext())
        {
            ctx.Database.Initialize(false);
        }
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<UserRole> UserRoles { get; set; }
}

Porque não armazenar os Roles no Cookie de Autenticação?

Cookies possuem um limite de 4KB, e dependendo da quantidade de informações que você desejar armazenar no cookie, esse limite pode acabar sendo ultrapassado. Um outro motivo para evitar armazenar esse tipo de informação em cookie é segurança: caso você faça modificações para as permissões do usuário, elas só entrarão em vigor no próximo login ou após o cookie expirar, o que em alguns casos pode não ser desejado, por exemplo, se você precisar remover uma permissão considerada importante para determinado usuário.

Verificando as permissões

Em qualquer Controller de uma aplicação ASP.NET MVC, podemos utilizar o atributo [Authorize]:

[Authorize(Roles = "Admin")]
public ActionResult Admin()
{
    return View();
}

Esse é um atributo que já existe no MVC. Antes de executar a Action acima, o framework irá verificar se a instância do usuário atual se refere a um usuário autenticado, e se possui o Role “Admin”. O código poderia ser reescrito da seguinte maneira, sem o atributo:

        public ActionResult Admin()
        {
            if (User.Identity.IsAuthenticated)
            {
                if (User.IsInRole("Admin"))
                {
                    return View();
                }
            }
            return new HttpUnauthorizedResult("Você não possui permissões para acessar este recurso!");s
        }

Conclusão

Apresentei uma solução completa para autenticação e autorização, espero que tenham gostado!

Todo o código deste post foi disponibilizado em um repositório público no BitBucket. Sinta-se a vontade para baixar e customizá-lo.

Dica do Dia: Evitando erros 404 para arquivos estáticos no ASP.NET MVC

Arquivos estáticos, ou seja, arquivos com extensão js, css, jpg, gif, etc. normalmente (salvo alterações no web.config) são servidos pelo StaticFileModule, que é um módulo nativo do IIS, ou pelo StaticFileHandler em aplicações ASP.NET. Caso o arquivo não exista em disco, o módulo envia uma resposta com status code 404 (not found) ao cliente, notificando que o arquivo não existe.

Em alguns casos, pode ocorrer de um módulo gerenciado (isto é, não nativo) interceptar a requisição e tentar servir o arquivo, por exemplo se o Web.Config possuir a configuração runAllManagedModulesForAllRequests=”true”:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
    <!-- ... -->
    </modules>
</system.webServer>

Com essa configuração acima (não recomendada), todos os módulos carregados pela aplicação irão participar da requisição, incluindo o UrlRoutingModule-4.0.

Nesse caso, o UrlRoutingModule também entrará em ação, tentando procurar uma rota que satisfaça a condição da Url da requisição atual. Em aplicações ASP.NET MVC, o módulo UrlRouting é responsável por instanciar o MvcHandler que irá iniciar a Pipeline do ASP.NET MVC (não confundir com a pipeline do ASP.NET). De forma bem resumida, a pipeline do ASP.NET MVC é a seguinte:

  • Encontrar e instanciar o controller baseado nas informações da rota (IControllerFactory.CreateController).
  • Encontrar e executar uma action no controller.
  • Renderizar uma ActionResult (escrever no Response.Output).

Caso haja uma requisição para um arquivo estático, e esse arquivo estático não existir em disco, dependendo da configuação do Web.Config e da tabela de rotas do UrlRoutingModule, pode ocorrer de a requisição entrar na pipeline do ASP.NET MVC. Nesse caso, é muito provável que o MvcHandler não encontro o controller que atenda a requisição, e lance a seguinte exception:

    if (controllerType == null)
    {
        throw new HttpException(404,
            String.Format(CultureInfo.CurrentCulture,         
            MvcResources.DefaultControllerFactory_NoControllerFound,
            requestContext.HttpContext.Request.Path));
    }

Se você utiliza o evento Application_Error no arquivo global.asax.cs ou em algum módulo que você criou, para logar os erros da aplicação (enviando emails ao administrador ou developer, por exemplo), perceberá com o tempo que haverá muitos logs inúteis.

Para evitar essas exceptions, a dica é muito simples: adicione uma rota do tipo IgnoreRoute contendo uma expressão regular que faz com que o UrlRouting module ignore urls que contenham caminhos para arquivos estáticos:

/*Global.asax.cs ou RouteConfig.cs*/

    var routes = RouteTable.Routes;

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{*staticfile}", 
        new { staticfile = @".*.(?i)(asp|aspx|css|js|xml|txt|png|gif|
              jpg|jpeg|bmp|ico|woff|svg|ttf|eot)(/.*)?" });

Com a adição dessas rota, requisições para arquivos estáticos nunca mais farão com que o ASP.NET inicie a pipeline do ASP.NET MVC, evitando o lançamento de exceptions inúteis.

Recomendo também adicionar essas rotas:

    routes.IgnoreRoute("Content/{*pathInfo}");
    routes.IgnoreRoute("Scripts/{*pathInfo}");
    routes.IgnoreRoute("Bundles/{*pathInfo}");

Isso faz com que nenhum arquivo dentro destas pastas sejam servidos na pipeline do MVC.

Recomendo a leitura de:
Caveats with the runAllManagedModulesForAllRequests in IIS 7/8

How ASP.NET MVC Routing Works and its Impact on the Performance of Static Requests

Optimize the performance of your web applications: Don’t use runAllManagedModulesForAllRequests=”true”.

Running Static Files Through VirtualPathProvider in IIS7

Dica do dia: AppSettings customizado em arquivos *.config

Normalmente utilizamos a seção AppSettings do arquivo Web.Config ou App.Config para criar parâmetros de configuração. E é justamente pra isso que ela existe: facilitar a leitura de parâmetros para serem consumidos pela aplicação. Se você não faz isso, provavelmente vai precisar um dia (quando estiver mais experiente, por exemplo). É sempre uma boa prática parametrizar configurações ao invés de escrever diretamente no código.

Exemplo:

    <appSettings>
        <add key="webpages:Version" value="3.0.0.0" />
        <add key="webpages:Enabled" value="true" />
        <add key="enableSimpleMembership" value="false" />
        <add key="ClientValidationEnabled" value="true" />
        <add key="UnobtrusiveJavaScriptEnabled" value="true" />

        <add key="Environment" value="Debug" />
        <add key="aspnet:UseHostHeaderForRequestUrl" value="true" />
        <add key="aspnet:FormsAuthReturnUrlVar" value="Next" />
    </appSettings>

A leitura dos valores da seção AppSettings é trivial:

    var valor = ConfigurationManager.AppSettings["chave"];

Se você utiliza MUITOS parâmetros, o ideal é criar uma seção específica para a aplicação, para evitar possíveis conflitos com configurações de outros frameworks que podem ocasionalmente utilizar a mesma chave “key”.

Aqui vai a dica, e é muito simples: Declare uma section do tipo AppSettingsSection. No exemplo, utilizei “myapp” como nome da section:

    <configSections>
        <section name="myapp" type="System.Configuration.AppSettingsSection"/>
    </configSections>

Em seguida, adicione suas configurações personalizadas numa section, similar ao appSettings:

    <myapp>
        <add key="email:admin" value="john@smith.com"/>
        <add key="email:developer" value="mary@jane.com"/>
    </myapp>

Na hora de ler, basta recuperar a section, fazendo um cast para NameValueCollection, da seguinte forma:

using System.Configuration;
using System.Collections.Specialized;

   var myapp = (NameValueCollection)ConfigurationManager.GetSection("myapp");
   var email = myapp["email:admin"];
   Trace.TraceInformation(email);

Lembre-se, substitua o “myapp” pelo nome que você desejar.

Tratamento efetivo de Exceções com ASP.NET (WebPages, MVC, WebForms)

Nest post mostrarei como obter total controle sobre exceções que ocorrem em aplicações ASP.NET.

Com o código que será mostrado aqui, será possível:

  • Controlar quais páginas o IIS exibirá
  • Controlar quais os erros a aplicação deve tratar
  • Exibir páginas customizadas e dinâmicas
  • Exibir uma página específica para um tipo de Exception customizada.
  • Decidir se irá logar o erro ou não

As variáveis para tomada de decisões são:

  • Ambiente de desenvolvimento ou produção
  • Código do Status (404 ou 500)
  • Tipo de Exception que capturada pela aplicação

Regras

Primeiro, vamos definir as seguintes regras para a implementação de tratamento de erros para todos os Requests:

  1. Nunca efetuar redirecionamento (HTTP/302) seja qual for a Exception.
  2. Nunca alterar a URL da solicitação, mantendo sempre a URL original.
  3. Nunca retornar HTTP/200 quando houver Exceptions, ou seja, retornar sempre o status correto.
  4. Não instanciar um Controller só para tratar uma Exception, evitando todo o “overhead” adicional.
  5. Retornar sempre conteúdo compatível com o que foi solicitado, por exemplo, não enviar uma página HTML como resposta caso a requisição seja AJAX, e não enviar conteúdo HTML para solicitação de arquivos de imagens/estáticos.
  6. Para Exceptions com StatusCode HTTP/404:
    a. Se o Request for para uma página (ou seja, contendo no Header “Accept: text/html”), retornar uma página genérica, informando que o recurso não existe;
    b. Se o Request for para um arquivo estático (imagens, javascript, css, etc), retornar Status 404, sem conteúdo.
    c. Se o request for uma chamada Ajax (ou seja, contendo no Header “X-Requested-With=XMLHttpRequest”), retornar uma mensagem curta, informando que o recurso solicitado não existe;
  7. Para Exceptions com StatusCode HTTP/500:
    a. Se o Request for para uma página (ou seja,contendo no Header “Accept: text/html”), retornar uma página genérica, informando que ocorreu um erro, sem detalhes;
    b. Se o request for uma chamada Ajax (ou seja, contendo no Header “X-Requested-With=XMLHttpRequest”), retornar uma mensgem curta, informando o erro de forma resumida;
    c. Para qualquer outro tipo de recurso solicitado, retornar apenas Status 404, sem conteúdo.
    d. Caso a Exception seja de um determinado tipo que eu tiver especificado, quero exibir uma página especial, por exemplo, quando uma exception do tipo BusinessRuleException for lançada, quero exibir este erro. Para todas as outras exceptions, retornar a página de erro padrão.

Essas regras otimizam a performance do servidor, enviando conteúdo relevante ao tipo do Request, e possibilitam uma melhor indexação por Search Engines (SEO), pois os Responses com código 404 não são indexados.

Implemetação

Definidas as regras, vamos para a implementação:

Começando pelas páginas que exibirão mensagens genéricas: 404.cshtml e 500.cshtml.

404.cshtml

Veja o código para 404.cshtml:

@{
    //404.cshtml
    Layout = null;
    Response.StatusCode = 404;
    if (IsAjax)
    {
        Response.Write("Resource not found");
        return;
    }
    const string html = "text/html";
    bool requestingHtml = Request.AcceptTypes.Any(type =>
        html.Equals(type, StringComparison.OrdinalIgnoreCase));

    if (!requestingHtml)
    {
        //não renderiza conteúdo
        return;
    }
}
    <!DOCTYPE html>
    <html>
        <head>
            <title>404 - Not Found</title>
        </head>
        <body>
            <p>@Request.RawUrl</p>
            <p>Este recurso não existe</p>
        </body>
    </html>

O código é simples. Primeiro setamos o status apropriado, verificamos se o request é uma chamada ajax, e se foi solicitada uma página html. Só então imprimimos o HTML caso necessário.

500.cshtml

Agora veja o código para 500.cshtml

@{
    Layout = null;
    Response.StatusCode = 500;
    if (IsAjax)
    {
        Response.Write("Server Error.");
        return;
    }
    const string html = "text/html";
    bool requestingHtml = Request.AcceptTypes.Any(type =>
        html.Equals(type, StringComparison.OrdinalIgnoreCase));

    if (!requestingHtml)
    {
        //não renderiza conteúdo
        return;
    }
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Erro interno do servidor</title>
</head>
<body>
    <h2>Ocorreu um erro. Desculpe-nos pelo incoveniente. O administrador do site foi alertado.</h2>
    <h3>@Session["exception"]</h3>
    <h4>Original Url: @Request.RawUrl</h4>
</body>
</html>

Aqui fizemos as mesmas verificações, exceto que imprimimos o valor de uma chave da Session[“exception”] caso exista. Utilizei aqui a Session pois é a única forma de fazer “comunicação” com a página, já que ela não possui Model, como nas views do Mvc. O valor da Session conterá a exception que eu passarei durante o Evento Application_Error.

Web.Config

Agora precisamos inserir algumas linhas no arquivo Web.Config para que estas páginas sejam retornadas pelo IIS:

<appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="true" />
</appSettings>

<system.web>
    <customErrors mode="RemoteOnly" />
</system.web>

<system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Auto" defaultResponseMode="ExecuteURL">
      <remove statusCode="404"/>
      <error statusCode="404" path="/404.cshtml" responseMode="ExecuteURL"/>
      <remove statusCode="500"/>
      <error statusCode="500" path="/500.cshtml" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Basicamente, estas linhas ativam o webpages (extensão .cshtml) que renderizarão os erros dinamicamente (não são páginas estáticas), e mapeiam os erros pelo status code para as páginas correspondentes.

Já é possível testar o funcionamento dessas páginas, basta rodar a aplicação e digitar URL’s inexistentes no browser para ver a página correspondete ao erro 404. Para ver a página de erro 500, lance uma Exception em qualquer código na aplicação.

BusinessRuleException.cs

Atendendo a uma das regras que especifiquei acima, utilizarei uma classe base para as exceptions que eu quero exibir de forma customizada. Somente exceptions deste tipo terão as mensagens impressas na página de erro. As outras exceptions, omitirei o erro, por motivos óbvios: Você não vai querer expor detalhes da aplicação em produção.

using System;
namespace WebApplication1
{
    public class BusinessRuleException : Exception
    {
        public BusinessRuleException(string msg):
            base(msg)
        {
        }
    }
}

HomeController.cs

Para testar as exceções, utilizarei um controller com os seguintes métodos:

using System;
using System.Web.Mvc;
using System.Web.WebPages;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            if (Request.IsAjaxRequest())
            {
                return Json(new { success = true }, JsonRequestBehavior.AllowGet);
            }
            return View();
        }

        public ActionResult Exception()
        {
            throw new Exception("Exception Controller... ");
        }

        public ActionResult BusinessException()
        {
            throw new BusinessRuleException("Ocorreu um erro de regra de negócios ...");
        }

        public ActionResult Status(int? status)
        {
            Response.SetStatus(status.GetValueOrDefault());
            return View("Index");
        }

        public ActionResult NotFound()
        {
            return HttpNotFound("Status 404 retornado pelo controller");
        }
    }
}

Global.asax.cs

Como de costume, utilizaremos o Application_Error em global.asax.cs para alterar o comportamento das Exceptions.

        protected void Application_Error(object sender, EventArgs e)
        {
            //Tratar exceções aqui
        }

ExceptionHandler.cs

Quando escrevi o tratamento de exceções no evento Application_Error, percebi que poderia criar uma classe reaproveitável:

using System;
using System.Diagnostics;
using System.Web;
using System.Web.WebPages;

namespace WebApplication1
{
    public class ExceptionHandler<TException> : IDisposable
        where TException : Exception
    {
        protected readonly HttpApplication Application;
        protected readonly string ErrorViewPath;
        protected readonly Action<HttpException> LogAction;

        public ExceptionHandler(HttpApplication application, string errorViewPath)
            : this(application, errorViewPath, exception => Trace.TraceError(exception.Message))
        {
        }

        public ExceptionHandler(HttpApplication application, string errorViewPath, Action<HttpException> logAction)
        {
            Application = application;
            ErrorViewPath = errorViewPath;
            LogAction = logAction;
        }

        public virtual void HandleError()
        {
            var server = Application.Server;
            var response = Application.Response;
            Exception ex = server.GetLastError();
            HttpException httpException = ex as HttpException ?? new HttpException("Unknown exception...", ex);

            var rootException = httpException.GetBaseException();
            Trace.TraceError("Exception: {0}", rootException.Message);
            if (IsProduction())
            {
                //log or send email to developer notifiying the exception ?
                LogAction(httpException);
                server.ClearError();
            }
            var statusCode = httpException.GetHttpCode();

            //setar o statuscode para que o IIS selecione a view correta (no web.config)
            response.StatusCode = statusCode;
            response.StatusDescription = rootException.Message; //todo: colocar uma msg melhor

            switch (statusCode)
            {
                case 404:
                    break; //IIS will handle 404
                case 500:
                    {
                        //check for exception type you want to show custom rendering
                        if (!(rootException is TException))
                        {
                            break; //IIS will handle 500
                        }
                        server.ClearError();
                        response.TrySkipIisCustomErrors = true;
                        response.Clear();

                        try
                        {
                            RenderException(rootException as TException);
                        }
                        catch
                        {
                            //fallback to response.Write
                            response.Write(rootException.ToString());
                        }
                        break;
                    }
            }
        }

        /// <summary>
        /// retorna true se app está em produção
        /// </summary>
        /// <returns></returns>
        protected virtual bool IsProduction()
        {
            return Application.Context.IsCustomErrorEnabled;
        }

        /// <summary>
        /// Overridable Method. Default implementation uses Razor WebPages.
        /// </summary>
        /// <param name="exception"></param>
        protected virtual void RenderException(TException exception)
        {
            //stores exception in session for later retrieve
            Application.Session["exception"] = exception;

            //executa a página
            var handler = WebPageHttpHandler.CreateFromVirtualPath(ErrorViewPath);
            handler.ProcessRequest(Application.Context);

            Application.Session.Remove("exception");
        }

        public virtual void Dispose()
        {
            Application.CompleteRequest();
        }
    }
}

Basicamente, a classe faz o seguinte:

  • Possui um construtor que recebe como parâmetro genérico o tipo de exception que utilizaremos para exibir mensagens customizadas, e outro parâmetro com a URL da página customizada (“~/Error.cshtml”).
  • Captura a Exception ocorrida, transformando-a numa Exception do tipo HttpException caso necessário.
  • Verifica se a aplicação está em ambiente de produção, caso positivo, chama o método que irá Logar a Exception (não faz sentido logar exceptions em ambiente de desenvolvimento).
  • Seta o StatusCode e StatusDescription para o Response.
  • Caso o StatusCode seja igual a 404, sai do método e entrega o erro ao IIS (que por sua vez irá exibir a página 404.cshtml que foi configurada no Web.Config).
  • Caso o Status seja 500, verifica se a exception é do tipo que foi especificado anteriormente (no construtor / parâmetro genérico). Caso negativo, entrega o erro ao IIS (que exibirá a página configurada, no caso, 500.cshtml). Caso positivo, chama o método RenderException(). Neste método, instancia-se a página informada no construtor, e passa via Session as informações que serão impressas.

CustomExceptionHandler.cs

Esta classe pode ser customizada com a exception desejada, no caso, BusinessRuleException.

using System;
using System.Diagnostics;
using System.Web;

namespace WebApplication1
{
    public class CustomExceptionHandler : ExceptionHandler<BusinessRuleException>
    {
        public CustomExceptionHandler(HttpApplication application, string errorViewPath)
            : base(application, errorViewPath, LogException)
        {
        }

        static void LogException(Exception exception)
        {
            //send email here...
            Trace.TraceError(exception.Message);
        }

        public override void HandleError()
        {
            base.HandleError();
        }

        protected override bool IsProduction()
        {
            return base.IsProduction();
        }

        protected override void RenderException(BusinessRuleException exception)
        {
            base.RenderException(exception);
        }
    }
}

Global.asax.cs

Finalmente, em Global.asax.cs, acrescente no evento Application_Error:

        protected void Application_Error(object sender, EventArgs e)
        {
            using (var customHandler = new CustomExceptionHandler(this,
                "~/500.cshtml"))
            {
                customHandler.HandleError();
            }
        }

Agora podemos testar utilizando os métodos do Controller, conforme código acima:

/Home/Index : retorna Status 200
/Home/Exception: lança uma Exception, resultando no HTTP Status 500
/Home/BusinessException: lança uma BusinessException
/Home/NotFound: retorna 404

Fim

Um post resumido, ainda assim extenso, porém todos as regras definidas no início do post foram seguidas à risca e implementadas com sucesso.

Tratar exceções no ASP.NET deveria ser algo bem simples, mas infelizmente não é, principalmente se você utiliza um mix de tecnologias na mesma aplicação (MVC, WebPages, WebForms). Pesquisando sobre o assunto e testando várias configurações e diversas formas de tratar exceções, cheguei a uma solução satisfatória que atende praticamente todas as necessidades de uma aplicação ASP.NET, cobrindo todas as tecnologias disponíveis. Neste post resumi o que aprendi.

Criei uma solution do Visual Studio 2013 (sp3) com todo o código exibido neste post. Você pode contribuir e baixar no GitHub, clicando aqui.

Corrigindo referências de Nuget Packages quando se utiliza Git e Subtrees

No post anterior mostrei como reutilizar projetos em repositórios Git separados através de subtrees.

Esse post mostro uma dica muito simples para corrigir referências a NuGet packages quando se utiliza subtrees.

Quando se instala Nuget packages ao seu projeto no Visual Studio 2013, as referências aos assemblies são inseridas no arquivo .csproj utilizando caminho relativo. Por exemplo, vejamos uma estrutura de pastas que demonstra como ficaria as referências a uma solution comum com apenas um projeto no Visual Studio:

├── ProjectChicago/
│ ├── WebApplication1/
│ │ ├── WebApplication1.csproj
│ │ ├── packages.config
│ ├── Build.sln
│ ├── packages/
  • ProjectChicago é a pasta raiz do nosso projeto de exemplo;
  • WebApplication1 é um projeto do tipo Asp.Net Web Application;
  • Build.sln é a solution que referencia o projeto WebApplication1.csproj.

Quando você instalar um NuGet package em WebApplication1, o arquivo packages.config para o projeto terá uma entrada para o pacote instalado. O Visual Studio utilizará esta informação para restaurar este pacote durante o build da solution. Veja um exemplo de packages.config:

Install-Package EntityFramework
    <?xml version="1.0" encoding="utf-8"?>
    <packages>
        <package id="EntityFramework" version="6.1.1"
            targetFramework="net451" />
    </packages>

No momento da restauração/instalação do pacote, por default, os arquivos serão copiados para uma pasta packages, no mesmo diretório da solution que está sendo compilada.

Agora veja o arquivo WebApplication1.csproj após instalarmos o EntityFramework:

    <Reference Include="EntityFramework">
        <HintPath>..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll</HintPath>
    </Reference>

O problema é que a referência ao assembly que o package instalou é adicionada com um caminho relativo.

Digamos que você queira utilizar este projeto WebApplication1 dentro de uma subtree, em uma outra solution. A estrutura para isso seria a seguinte:

├── ProjectDetroit/
| ├── .git/
| ├── ProjectChicago/
| │ ├── WebApplication1/
| │ │ ├── WebApplication1.csproj
| │ │ ├── packages.config
│ ├── Build.sln
│ ├── packages/
  • Repare que agora ProjectChicago é uma subtree dentro de ProjectDetroit (tudo feito conforme post anterior).

O Visual Studio não conseguirá compilar WebApplication1 da pasta ProjectChicago pois devido ao caminho relativo do HintPath ..\\packages, a pasta não será encontrada.

Simples solução

Altera WebApplication1.csprojt e utilize a variável $(SolutionDir) ao invés de ..\ nos caminhos dos pacotes:

    <Reference Include="EntityFramework">
        <HintPath>$(SolutionDir)\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll</HintPath>
    </Reference>

Dessa maneira, ao utilizar o projeto dentro de uma subtree de outro repositório Git, a pasta packages será localizada com precisão e não haverá erros de build.

Trabalhando em Projetos compartilhados com Git e Subtrees

Segue uma dica muito interessante pra quem trabalha com múltiplos projetos e utiliza projetos compartilhados.

Tenha em mente a seguinte estrutura de diretório:

├── Projetos/
│ ├── MyFramework
│ │ ├── .git
│ │ ├── ...
│ ├── WebApplication1
│ │ ├── .git
│ │ ├── ...
│ ├── WebApplication2
│ │ ├── .git
│ │ ├── ...

Nota-se que:

  • Cada um dos 3 projetos possuem seus respectivos repositórios Git configurado com remotes;
  • MyFramework é uma pasta que contém código de um projeto compartilhado;
  • WebApplication1 contém um aplicação Web que faz uso de código contido na pasta MyFramework;
  • WebApplication2 contém outra aplicação que também utiliza código contido na pasta MyFramework;
  • Independente da linguagem utilizada nos projetos, assume-se que cada pasta seja um projeto separado;

Para que um outro programador possa rodar o projeto WebApplication1 em seu computador de desenvolvmento, será necessário clonar os 2 repositórios reproduzindo a mesma estrutura de diretório para obter sucesso (assumindo que as referências utilizam caminhos relativos, por exemplo, ..\MyFramework).

mkdir Projetos
cd Projetos
git clone http://path/to/remote/MyFramework.git
git clone http://path/to/remote/WebApplication1.git

Até então tudo bem, você tem repositórios para cada um de seus projetos, com projetos organizados de forma que seja possível evitar duplicidade de código, bastando que os projetos sejam clonados obedenco a mesma estrutura.

O problema começa quando você vai utilizar algum serviço de integração/build, onde você precisa publicar WebApplication1 em um servidor Web (AppHarbor ou Heroku, por exemplo). Estes serviços exigem que todo o código do projeto esteja disponível ao serem clonados/baixados do mesmo repositório Git antes de serem compilados/publicados.

Utilizando a estrutura de repositórios/diretórios exibida acima, isso não seria possível pois o código compartilhado está em um repositório diferente.

Uma solução rápida seria modificar a estrutura de diretório e as referências, fazendo uma cópia do código compartilhado dentro do repositório da aplicação que a utiliza, como demonstrado abaixo:

├── Projetos/
│ ├── MyFramework
│ │ ├── .git
│ ├── WebApplication1
│ │ ├── .git
│ │ │ ├── MyFramework (cópia ctrl+c/ctrl+v)
│ ├── WebApplication2
│ │ ├── .git
│ │ │ ├── MyFramework (cópia ctrl+c/ctrl+v)

Aí começa a bagunça. Caso você faça modificações em WebAppliction1 no código dentro da pasta da cópia de MyFramework, terá que replicar manualmente as mesmas alterações nas outras cópias e no projeto original, o que pode levar a uma falta de sincronia, erros, diferença de versões, etc.

Pesquisando por uma mais eficaz, encontrei duas opções: submodules e subtrees.

Testei primeiro submodules, e apesar do sucesso, acabei desistindo da idéia pois os serviços de integração não funcionam muito bem com submodules, pois o código do submodule não fica disponível imediatamente após o clone do repositório, sendo necessário algum trabalho adicional que o AppHarbor/BitBucket não fazem.

Obtive sucesso com subtree.

De forma simples e resumida, uma subtree é uma espécie de fork de um outro repositório que passa a fazer parte do repositório principal. A vantagem em relação aos submodules é que o código está disponível imediatamente após o clone/checkout.

Com subtree, a estrutura passa a ser assim:

├── Projetos/
│ ├── MyFramework
│ │ ├── .git
│ ├── WebApplication1
│ │ ├── .git
│ │ │ ├── MyFramework (fork do repositório original)
│ │ | | ├── .git
│ ├── WebApplication2
│ │ ├── .git
│ │ │ ├── MyFramework (fork do repositório original)
│ │ | | ├── .git

Segue os comandos para trabalhar com subtree em um repositório git existente (o repositório deve estar sincronizado antes de adicionar uma subtree, certifique-se de efetuar um commit antes):

cd WebApplication1
git remote add --fetch MyFramework_Pull http://path/to/remote/MyFramework.git
git subtree add --prefix=MyFramework MyFramework_Pull master --squash

Pronto, agora você tem uma cópia local do repositório, de forma sincronizada com o repositório original de MyFramework.

Quando houver novos commits no repositório original de MyFramework, para sincronizar na subtree atual, execute o seguinte:

git fetch MyFramework_Pull master
git subtree pull --prefix=MyFramework MyFramework_Pull master --squash

Quando você alterar sua cópia local de MyFramework, e quiser atualizar para o repositório original, você irá precisar de adicionar um remote específico para fazer push antes:

git remote add MyFramework_Push http://path/to/remote/MyFramework.git

A partir de então, apenas execute:

git subtree push --prefix=MyFramework MyFramework_Push master

Lembrando que para tudo dar certo, certifique-se de que os repositórios possuam remote, senão você não conseguirá atualizar da subtree para o repositório original.

Um projeto que estou trabalhando e que utilizo subtrees está no BitBucket.