Cutting Edge

Filtros de ação no ASP.NET MVC

Dino Esposito

O maior desafio que muitos arquitetos de software enfrentam hoje é como projetar e implementar um aplicativo que possa atender a todos os requisitos da versão 1 e aos outros que poderão surgir depois. A facilidade de manutenção tem sido um dos atributos fundamentais do projeto de software desde o primeiro esboço do documento ISO/IEC 9126, em 1991. (O documento apresenta uma descrição formal da qualidade do software, que é dividida em um conjunto de características e subcaracterísticas, sendo que uma delas é a facilidade de manutenção. Uma versão em PDF do documento pode ser obtida em iso.org.)

A capacidade de atender às necessidades atuais e futuras de um cliente por certo não é um novo requisito para nenhum software. No entanto, o que muitos aplicativos Web exigem hoje é uma forma de facilidade de manutenção sutil e de curto prazo. Muitas vezes, os clientes não querem novas funções ou outra implementação de um recurso já existente. Eles só querem adicionar, substituir, configurar ou remover pequenas partes da funcionalidade. Um exemplo típico é quando sites com grandes públicos lançam campanhas publicitárias específicas. O comportamento geral do site não muda, mas ações extras devem ser executadas junto com ações existentes. Além disso, essas alterações não costumam ser permanentes. Elas devem ser removidas após algumas semanas para então serem reincorporadas alguns meses depois, serem configuradas de modo diferente e assim por diante. Você precisa ter a habilidade de programar qualquer funcionalidade necessária combinando pequenas partes, controlar as dependências sem afetar muito o código-fonte e buscar um projeto de software orientado a aspectos. Esses são alguns dos principais motivos por detrás da rápida adoção de estruturas IoC (Inversão de Controle) em muitos projetos empresariais.

Então, do que trata este artigo? Ele não será uma preleção maçante sobre como o software está mudando nos dias de hoje. Pelo contrário, este artigo é uma análise profunda de um avançado recurso dos controladores do ASP.NET MVC que pode ajudá-lo particularmente na compilação de soluções da Web orientadas a aspectos: os filtros de ação do ASP.NET MVC.

Em todo caso, o que é um filtro de ação?

Um filtro de ação é um atributo que, quando anexado a uma classe de controlador ou a um método de controlador, proporciona uma forma declarativa de vincular algum comportamento a uma ação solicitada. Criando um filtro de ação, você pode vincular o pipeline de execução de um método de ação e adaptá-lo às suas necessidades. Dessa forma, também é possível remover da classe de controlador qualquer lógica que não pertença estritamente ao controlador. Fazendo isso, você torna esse comportamento específico reutilizável e, o mais importante, opcional. Os filtros de ação são ideais para implementar questões abrangentes que afetam a vida dos seus controladores.

O ASP.NET MVC vem com alguns filtros de ação predefinidos, como HandleError, Authorize e OutputCache. Você pode usar HandleError para interceptar exceções geradas durante a execução de métodos na classe de controlador de destino. A interface de programação do atributo HandleError permite indicar a exibição de erro a ser associada com um dado tipo de exceção.

O atributo Authorize bloqueia a execução de um método para usuários não autorizados. Entretanto, ele não diferencia usuários que ainda não se conectaram de usuários conectados que não têm permissões suficientes para executar uma dada ação. Na configuração do atributo, é possível especificar quaisquer funções necessárias para executar uma determinada ação.

O atributo OutputCache armazena em cache a resposta do método do controlador referente à duração especificada e a lista de parâmetros solicitada.

Uma classe de filtro de ação implementa várias interfaces. A lista completa de interfaces é apresentada na Figura 1.

Figura 1 Interfaces para um filtro de ação

Interfaces de filtro Descrição
IActionFilter Métodos na interface são chamados antes e depois da execução do método do controlador.
IAuthorizationFilter Métodos na interface são chamados antes da execução do método do controlador.
IExceptionFilter Métodos na interface são chamados sempre que uma exceção é gerada durante a execução do método do controlador.
IResultFilter Métodos na interface são chamados antes e depois do processamento do resultado da ação.

Nos cenários mais comuns, a preocupação maior é com IActionFilter e IResultFilter. Vamos conhecê-los mais de perto. Esta é a definição da interface IActionFilter:

public interface IActionFilter
{
  void OnActionExecuted(ActionExecutedContext filterContext);
  void OnActionExecuting(ActionExecutingContext filterContext);
}

Você implementa o método OnActionExecuting para executar código antes da ação do controlador ser executada; você implementa OnActionExecuted para fazer o pós-processamento do estado de controlador determinado por um método. Os objetos de contexto fornecem muitas informações do tempo de execução. Esta é a assinatura de ActionExecutingContext:

public class ActionExecutingContext : ControllerContext
{
  public ActionDescriptor ActionDescriptor { get; set; }
  public ActionResult Result { get; set; }
  public IDictionary<string, object> ActionParameters { get; set; }
}

O descritor da ação, em particular, fornece informações sobre o método de ação, como seu nome, controlador, parâmetros, atributos e filtros adicionais. A assinatura de ActionExecutedContext só é um pouco diferente, como vemos aqui:

public class ActionExecutedContext : ControllerContext
{
  public ActionDescriptor ActionDescriptor { get; set; }
  public ActionResult Result { get; set; }
  public bool Canceled { get; set; }
  public Exception Exception { get; set; }
  public bool ExceptionHandled { get; set; }
}

Além de uma referência à descrição da ação e ao resultado da ação, a classe fornece informações sobre uma exceção que poderia ter ocorrido e oferece dois sinalizadores booleanos que merecem mais atenção. O sinalizador ExceptionHandled indica que o seu filtro de ação tem a oportunidade de marcar uma exceção ocorrida como tendo sido tratada. O sinalizador Canceled é referente à propriedade Result da classe ActionExecutingContext.

Observe que a propriedade Result na classe ActionExecutingContext só existe para transferir o trabalho de gerar qualquer resposta de ação do método do controlador para a lista de filtros de ação registrados. Se algum filtro de ação atribuir um valor à propriedade Result, o método de destino na classe do controlador nunca será chamado. Fazendo isso, você ignora o método de destino, transferindo o trabalho de gerar uma resposta inteiramente para os filtros de ação. Porém, se vários filtros de ação foram registrados para um método de controlador, eles compartilhariam o resultado da ação. Quando um filtro define o resultado da ação, todos os filtros subsequentes na cadeira receberão o objeto ActionExecuteContext com a propriedade Canceled definida como true. Independentemente de você definir Canceled programaticamente na etapa executada pela ação ou de configurar a propriedade Result na etapa de execução da ação, o método de destino nunca será executado.

Criando filtros de ação

Como mencionado, quando se trata de criar filtros personalizados, quase sempre você está interessado em filtros que fazem o pré e o pós-processamento do resultado da ação e em filtros que executam antes e depois da execução do método de controlador regular. Em vez de implementar interfaces nativamente, uma classe de filtro de ação normalmente é derivada de ActionFilterAttribute:

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
  public virtual void OnActionExecuted(ActionExecutedContext filterContext);
  public virtual void OnActionExecuting(ActionExecutingContext filterContext);
  public virtual void OnResultExecuted(ResultExecutedContext filterContext);
  public virtual void OnResultExecuting(ResultExecutingContext filterContext);
}

Você substitui OnActionExecuted para adicionar um código personalizado à execução do método. Você substitui OnActionExecuting como pré-condição para a execução do método de destino. Por último, você substitui OnResultExecuting e OnResultExecuted para colocar o código em torno da etapa interna que determina a geração da resposta do método.

A Figura 2 mostra um exemplo de filtro de ação que adiciona compactação programaticamente à resposta do método ao qual é aplicado.

Figura 2 Um exemplo de filtro de ação para compactar a resposta do método

public class CompressAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(
    ActionExecutingContext filterContext)
  {
    // Analyze the list of acceptable encodings
    var preferred = GetPreferredEncoding(
      filterContext.HttpContext.Request);

    // Compress the response accordingly
    var response = filterContext.HttpContext.Response;
    response.AppendHeader("Content-encoding", preferred.ToString());

    if (preferredEncoding == CompressionScheme.Gzip)
    {
      response.Filter = new GZipStream(
        response.Filter, CompressionMode.Compress);
    }

    if (preferredEncoding == CompressionScheme.Deflate)
    {
      response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
    }
    return;
  }

  static CompressionScheme GetPreferredEncoding(HttpRequestBase request)
  {
    var acceptableEncoding = request.Headers["Accept-Encoding"];
    acceptableEncoding = SortEncodings(acceptableEncoding);

    // Get the preferred encoding format 
    if (acceptableEncoding.Contains("gzip"))
      return CompressionScheme.Gzip;
    if (acceptableEncoding.Contains("deflate"))
      return CompressionScheme.Deflate;

    return CompressionScheme.Identity;
  }

  static String SortEncodings(string header)
  {
    // Omitted for brevity
  }
}

No ASP.NET, normalmente a compactação é obtida registrando-se um módulo HTTP que intercepta quaisquer solicitações e compacta as respectivas respostas. Como alternativa, você também pode habilitar a compactação no nível do IIS. ASP.NET MVC dá suporte aos dois cenários e também oferece uma terceira opção: controlar a compactação por método. Dessa forma, é possível controlar o nível de compactação relativo a uma URL específica sem a necessidade de criar, registrar e manter um módulo HTTP.

Como você pode ver na Figura 2, o filtro de ação substitui o método OnActionExecuting. A princípio isso pode parecer um pouco estranho, porque talvez você espere que a compactação seja uma questão abrangente tratada bem antes de retornar alguma resposta. A compactação é implementada através da propriedade Filter de HttpResponse intrínseco. Qualquer resposta que esteja sendo elaborada pelo ambiente de tempo de execução é retornada ao navegador cliente por meio do objeto HttpResponse. Subsequentemente, qualquer fluxo personalizado montado no fluxo de saída padrão através da propriedade Filter pode alterar a saída que está sendo enviada. Portanto, no decurso do método OnActionExecuting, você apenas configura fluxos adicionais sobre o fluxo de saída padrão.

No entanto, quando se trata de compactação HTTP, a parte mais difícil é considerar atentamente as preferências do navegador. O navegador envia suas preferências de compactação por meio do cabeçalho Accept-Encoding. O conteúdo do cabeçalho indica que o navegador pode lidar apenas com certas codificações, geralmente gzip e deflate. Para funcionar bem, o seu filtro de ação deve tentar adivinhar exatamente com o que o navegador pode lidar. Essa pode ser uma tarefa trabalhosa. A função do cabeçalho Accept-Encoding é explicada em detalhes no RFC 2616 (consulte w3.org/Protocols/rfc2616/rfc2616-sec14.html). Em resumo, o conteúdo do cabeçalho Accept-Encoding pode ter um parâmetro q, cuja finalidade é atribuir uma prioridade a um valor aceitável. Por exemplo, considere que todas as cadeias de caracteres a seguir são valores aceitáveis para uma codificação, mas embora gzip aparentemente seja a primeira opção em todas elas, somente na primeira ele é a opção favorita:

gzip, deflate
gzip;q=.7,deflate
gzip;q=.5,deflate;q=.5,identity

Um filtro de compactação deve considerar essa fato, como faz o filtro mostrado na Figura 2. Esses detalhes devem reforçar a ideia de que, ao criar um filtro de ação, você está interferindo no tratamento da solicitação. Subsequentemente, tudo o que você fizer deve ser coerente com a expectativa do navegador cliente.

Aplicando um filtro de ação

Como já mencionado, um filtro de ação é simplesmente um atributo que pode ser aplicado a métodos individuais e à classe pai inteira. Veja como configurá-lo:

[Compress]
public ActionResult List()
{
  // Some code here
  ...
}

Se a classe do atributo contém algumas propriedades públicas, é possível atribuir valores a elas declarativamente usando a notação de atributos já conhecida:

[Compress(Level=1)]
public ActionResult List()
{
  ...
}

A Figura 3 mostra a resposta compactada como relatado pelo Firebug.

image: A Compressed Response Obtained via the Compress Attribute

Figura 3 Uma resposta compactada obtida através do atributo Compress

Um atributo ainda é uma forma estática de configurar um método. Isso significa que você precisa de uma segunda etapa de compilação para aplicar mais alterações. Porém, os filtros de ação expressos na forma de atributos oferecem um benefício importante: eles mantêm as questões abrangentes fora do método de ação principal.

Uma análise mais direta dos filtros de ação

Para avaliar a força real dos filtros de ação, pense em aplicativos que precisam de muita personalização com o passar do tempo e em aplicativos que exigem adaptação quando instalados para diferentes clientes.

Imagine, por exemplo, um site que, em algum momento, lança uma campanha publicitária com base nos pontos de recompensa que um usuário pode ganhar se executar alguma ação padrão em um site (comprar mercadorias, responder perguntas, bater papo, blogar e assim por diante). Como desenvolvedor, provavelmente você precisa de um código que seja executado após a execução do método comum de executar uma transação, postar um comentário ou iniciar um bate papo. Infelizmente, esse código é instável e dificilmente deve fazer parte da implementação original do método de ação principal. Com os filtros de ação, você pode criar diferentes componentes para cada cenário exigido e, por exemplo, providenciar um filtro de ação para adicionar pontos de recompensa. Em seguida, você pode anexar o filtro de recompensa a qualquer método no qual a ação de postar seja necessária; depois você recompila e continua.

[Reward(Points=100)]
public ActionResult Post(String post)
{
  // Core logic for posting to a blog
  ...
}

Como mencionamos, os atributos são estáticos e exigem uma etapa adicional de compilação. Embora talvez isso não seja desejável em todos os casos (digamos, em sites com recursos altamente voláteis), é bem melhor do que nada. No mínimo, você ganha a capacidade de atualizar soluções da Web rapidamente e com baixo impacto sobre os recursos existentes, o que é sempre bom para manter as falhas de regressão sob rigoroso controle.

Carregamento dinâmico

Este artigo demonstrou os filtros de ação no contexto de métodos de ação de controlador. Eu demonstrei uma abordagem canônica para criar filtros como atributos que podem ser usados para decorar métodos de ação estaticamente. No entanto, há uma pergunta subjacente: É possível carregar filtros de ação dinamicamente?

A estrutura ASP.NET MVC consiste em um código bem-escrito (e grande), por isso expõe várias interfaces e métodos substituíveis com os quais você pode personalizar praticamente cada aspecto dela. Felizmente, essa tendência vai ser reforçada pelo próximo Model-View-Controller (MVC) 3. Segundo publicado, um dos objetivos da equipe para o MVC 3 é possibilitar a injeção de dependência em todos os níveis. Portanto, a resposta para a pergunta anterior sobre carregamento dinâmico está nas capacidades de injeção de dependência da estrutura MVC. Uma possível estratégia vencedora é personalizar o chamador da ação para obter acesso à lista de filtros antes da execução de um método. Como a lista de filtros se parece com um objeto de coleção de leitura/gravação básico, não deve ser difícil populá-la dinamicamente. Mas isso é assunto para uma nova coluna.

Dino Esposito é o autor de "Programming ASP.NET MVC" (Microsoft Press, 2010) e coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Residente na Itália, Esposito é um palestrante sempre presente em eventos do setor no mundo inteiro. Entre em contato com ele através de seu blog, em weblogs.asp.net/despos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Scott Hanselman