ASP.NET Web API – Estensibilidade e Arquitetura

Israel Aece

Julho 2013

Para que seja possível tirar um maior proveito do que qualquer biblioteca ou framework tem a oferecer, é termos o conhecimento mais profundo de sua arquitetura. Apesar de ser opcional no primeiro momento, é de grande importância o conhecimento destes mecanismos, pois podem ser úteis durante alguma depuração que seja necessária ou durante a estensão de algum ponto para uma eventual customização, algo que também será abordado neste capítulo.

O entendimento da arquitetura nos dará uma visão bem detalhada do processamento das mensagens, sabendo o ponto correto para interceptar uma requisição a fim customizar algum elemento, interferir na escolha de alguma ação, aplicação de segurança (autenticação e autorização), implementar uma camada de caching, etc. Muito desses pontos já vimos no decorrer dos capítulos anteriores, e a próprio Microsoft fez uso deles para implementar algum elementos que já estão embutidos no ASP.NET Web API.

Antes de falarmos sobre a arquitetura do ASP.NET Web API, precisamos recapitular – de forma resumida – como é a infraestrutura do ASP.NET, e depois disso, veremos a bifurcação onde temos o desvio para o MVC, Web Forms e Web API.

Tudo começa com a criação da classe HttpApplication, que é o objeto que irá coordenar e gerenciar toda a execução das requisições que chegam para uma aplicação. Dentro deste objeto temos uma coleção de módulos, que são classes que implementam a interface IHttpModule, que são como filtros onde podemos examinar e modificar o conteúdo da mensagem que chega e que parte através do pipeline.

Depois que a mensagem passar pelos módulos, chega o momento de escolher o handler que tratará a requisição. O handler é o alvo da requisição, que irá receber a requisição e tratá-la, e devolver a resposta. Os handlers implementam a interface IHttpHandler ou IHttpAsyncHandler (para implementação assíncrona), e existem diversas classes dentro do ASP.NET, onde uma trata a requisição para uma página do Web Forms, para uma aplicação MVC, para um serviço ASMX, etc. Depois do handler executado, a mensagem de retorno é gerada, passa pelos módulos que interceptam o retorno, e parte para o cliente que solicitou o recurso.

Dn376308.6B1BACF740B38853B5067D2B3D5985E7(pt-br,MSDN.10).png

Figura 22 - Caminho percorrido pela requisição nos módulos e handlers.

Independentemente de qual recurso será acessado, o estágio inicial é comum para todos eles. Tudo o que falamos até aqui vale para quando estamos utilizando o hosting baseado na web (IIS/ASP.NET). No caso de self-hosting, onde hospedamos a API em nosso próprio processo, o caminho para a ser igual.

Dn376308.992CA6CEA2B8DCD094D0E0376C7A4856(pt-br,MSDN.10).png

Figura 23 - Comparação entre web e self-hosting.

Como há comentado anteriormente, a classe HttpSelfHostServer (que herda da classe HttpServer), utilizada quando optamos pelo modelo de self-hosting, faz uso de recursos fornecidos pelo WCF, e depois de coletar as informações que chegam até o serviço, ele cria e passa adiante a instância da classe HttpRequestMessage.

É importante notar que do lado do web-hosting temos a classe HttpControllerHandler, que é a implementação da classe IHttpHandler, responsável por materializar a requisição (HttpRequest) no objeto do tipo HttpRequestMessage, enviando-a para a classe HttpServer.

Depois que a requisição pela classe HttpServer, um novo pipeline é iniciado, que possui vários pontos, e o primeiro deles é chamado de Message Handlers. Como o próprio nome diz, eles estão logo no primeiro estágio do pipeline, ou seja, independentemente de qual sua intenção para com o serviço, elas serão sempre serão executadas, a menos que haja algum critério que você avalie e rejeite a solicitação, o que proibirá o avanço do processamento para os próximos handlers.

Basicamente esses handlers recebem a instância de uma classe do tipo HttpRequestMessage, que traz toda a solicitação do usuário, e retornam a instância da classe HttpResponseMessage, contendo a resposta gerada para aquela solicitação. E como já ficou subententido, podemos ter vários handlers adicionados ao pipeline, onde cada um deles pode ser responsável por executar uma tarefa distinta, como logging, autenticação, autorização, etc. A imagem abaixo ilustra esse fluxo:

Dn376308.896597679E0B92384686A096B7D9851F(pt-br,MSDN.10).png

Figura 24 - Estrutura dos message handlers.

Para a criação que um message handler customizado, é necessário herdar da classe abstrata DelegatingHandler. Essa classe pode receber em seu construtor um objeto do tipo HttpMessageChannel. A finalidade deste objeto que é passado no construtor, é com o intuito de cada handler seja responsável por executar uma determinada tarefa, e depois passar para o próximo, ou seja, uma implementação do padrão Decorator.

public class ApiKeyVerification : DelegatingHandler
{
    private const string ApiKeyHeader = "Api-Key";
    private static string[] ValidKeys = new string[] { "18372", "92749" };

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (IsValidKey(request))
            return base.SendAsync(request, cancellationToken);

        return Task.Factory.StartNew(() => 
            new HttpResponseMessage(HttpStatusCode.Unauthorized));
    }

    private static bool IsValidKey(HttpRequestMessage request)
    {
        var header = request.Headers.FirstOrDefault(h => h.Key == ApiKeyHeader);

        return
            header.Value != null && 
            ValidKeys.Contains(header.Value.FirstOrDefault());
    }
}

A classe acima intercepta a requisição e procura se existe um header chamado Api-Key. Se não houver ou se existir e não for uma chamada válida, ele rejeita a requisição retornando o código 401 (Unauthorized) ao cliente, que significa que ele não está autorizado a visualizar o conteúdo. É importante ressaltar que se a chave não for válida, a requisição não vai adiante, ou seja, ela já é abortada quando a primeira inconsistência for encontrada.

Podemos acoplar os message handlers em dois níveis para serem executados. Eles podem ser globais, que como o próprio nome sugere, serão executadas para todas as requisições que chegam a API, ou serem específicos para uma determinada rota. No primeiro caso, a instância do message handler é adicionada à coleção de handlers, através da propriedade MessageHandlers. Já a segunda opção, recorremos à um overload do método MapHttpRoute, onde em seu último parâmetro temos a opção de incluir o message handler específico para ela. Abaixo temos o exemplo de como fazemos para utilizar uma ou outra opção:

    //Global
    config.MessageHandlers.Add(new ApiKeyVerification());
    
    //Por rota
    config.Routes.MapHttpRoute(
            name: "Default",
            routeTemplate: "api/{controller}",
            defaults: null,
            constraints: null,
            handler:
                HttpClientFactory.CreatePipeline(
                    new HttpControllerDispatcher(config),
                    new DelegatingHandler[] { new ApiKeyVerification() }));

Se encaminharmos a requisição com uma chave inválida, podemos perceber que não somos autorizados a acessar o recurso. A partir do momento que colocamos uma chave que o serviço entende como válida, o resultado é retornado. A imagem abaixo comprova essa análise.

Dn376308.CB9A9D5D0B9BDFAA40E4D7AE5FD02AB8(pt-br,MSDN.10).png

Figura 25 - Headers customizados para controle de acesso.

Existem dois message handlers embutidos no ASP.NET Web API que desempenham um papel extremamente importante no pipeline. O primeiro deles é HttpRoutingDispatcher, que avalia se existe um message handler específico para a rota que foi encontrada. Se houver, ele deve ser executado.

Caso não seja, a requisição e encaminhada para um outro message handler chamado HttpControllerDispatcher. Uma vez que passamos por todos os handlers configurados, este será responsável por encontrar e ativar o controller. No código acima mencionamos a classe HttpControllerDispatcher quando configuramos o message handler ApiKeyVerification em nível de rota.

A procura, escolha e ativação do controller são tarefas realizadas por elementos que também são estensíveis. Eles são representados pelas seguintes interfaces: IHttpControllerSelector e IHttpControllerActivator. Depois do controller encontrado, é o momento de saber qual ação (método) dentro dele será executada. Da mesma forma, se quisermos customizar, basta recorrer a implementação da interface IHttpActionSelector.

Acima vimos os message handlers no contexto do lado do servidor, mas pela simetria que existe na codificação do servidor comparado ao cliente, podemos recorrer aos mesmos recursos para interceptar e manipular tanto a requisição quanto a resposta do lado do cliente. O construtor da classe HttpClient pode receber como parâmetro a instância de uma classe do tipo HttpMessageHandler. Ao omitir qualquer inicialização, por padrão, o handler padrão é o HttpClientHandler, que herda de HttpMessageHandler, que é responsável pela comunicação com o HTTP, já em nível de rede.

Se quisermos customizar, incluindo handlers para analisar e/ou alterar a saída ou o retorno da requisição no cliente, podemos recorrer ao método de estensão chamado Create da classe HttpClientFactory, que recebe um array contendo todos os handlers que serão disparados, quais serão disparados em ordem inversa ao que é inserido na coleção. Este mesmo método faz o trabalho para – também – inserir o handler HttpClientHandler que se faz necessário em qualquer situação.

using (var client = 
    HttpClientFactory.Create(new ValidacaoDeSessao()))
{
    //...
}

public class ValidacaoDeSessao : DelegatingHandler
{
    //...
}

Dn376308.C2029D5C74B25CD43663FC6BF0CA4D63(pt-br,MSDN.10).png

Figura 26 - Estrutura dos message handlers do lado do cliente.

Apesar do controller e a ação dentro dele terem sido encontrados, podemos ainda realizar alguma espécie de interceptação para que ainda façamos alguma tarefa antes de executarmos a ação. Eis que surgem os filtros, que servem como uma forma de concetrar alguuns elementos de cross-cutting, como segurança, tradução de exceções em erros HTTP, etc.

Os filtros podem ser aplicados em ações específicas dentro do controller, no controller como um todo, ou para todas as ações em todos os controllers (global). O benefício que temos na utilização de filtros é a granularidade onde podemos aplicá-los. Talvez interromper a requisição logo nos primeiros estágios (via handlers) possa ser mais eficaz, pois muitas vezes você não precisa passar por todo o pipeline para tomar essa decisão.

Há um namespace chamada System.Web.Http.Filters, que possui vários filtros já predefinidos. Todo filtro herda direta ou indiretamente da classe abstrata FilterAttribute, que já possui a estrutura padrão para todos os filtros. O diagrama abaixo ilustra essa hierarquia.

Dn376308.59A3220DD68477EED7158DEE8FD040E9(pt-br,MSDN.10).png

Figura 27 - Hierarquia das classes dos filtros existentes dentro do framework.

Além da classe base para os filtros, já temos algumas outras, também abstratas, que definem a estrutura para que seja criado um filtro para controlarmos a autorização, outro para controlarmos o tratamento de erros e um que nos permite interceptar a execução de alguma ação dentro do controller.

Para e exemplificar a customização de um filtro, podemos criar um que impossibilite o acesso à uma ação se ela não estiver protegida por HTTPS. A classe ActionFilterAttribute fornece dois métodos que podemos sobrescrever na classe derivada: OnActionExecuting e OnActionExecuted. Como podemos perceber, um deles é disparado antes e o outro depois da ação executada.

public class ValidacaoDeHttps : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var request = actionContext.Request;

        if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response =
                request.CreateResponse(
                    HttpStatusCode.Forbidden,
                    new StringContent("É necessário que a requisição seja HTTPS."));
        }
    }
}

O fato da classe ActionFilterAttribute herdar da classe Attribute, podemos aplicar este atributo tanto no controller quanto em um ou mais ações, ou seja, podemos ter um refinamento mais apurado, pois temos condições de aplicar isso em certos casos, em outros não. No exemplo abaixo optamos apenas por proteger por HTTPS a ação Teste2. Se quisermos que todos as ações dentro deste controller sejam protegidas, basta apenas elevarmos o atributo, decorando a classe com o filtro criado.

public class TestController : ApiController
{
    [HttpGet]
    public string Teste1()
    {
        return "teste1";
    }

    [HttpGet]
    [ValidacaoDeHttps]
    public string Teste2()
    {
        return "teste2";
    }
}

Finalmente, se desejarmos que este atributo seja aplicado em todas as ações de todos os controllers, adicionamos este filtro em nível global, através da configuração da API:

    config.Filters.Add(new ValidacaoDeHttps());

Sobrescrita de Filtros

Ao aplicar o filtro em nível global, evita termos que decorarmos cada (nova) ação ou cada (novo) controller com um determinado atributo, evitando assim que, por algum descuido, um determinado código deixe de rodar antes e/ou depois de cada ação. Sendo assim, o nível global nos permite aplicar incondicionalmente para todas as ações, e se quisermos aplicar um filtro para uma ou outra ação, decoramos o atributo diretamente nele.

Só que ainda há uma outra situação, que é quando precisamos aplicar determinados filtros para a grande maioria das ações, mas em poucas delas não queremos que o filtro seja aplicado. Para isso, quando configurarmos um filtro em nível global, podemos sobrescrever uma determinada ação para que os filtros não sejam aplicados nela. Para isso, entra em cena um atributo chamado OverrideActionFiltersAttribute, que quando aplicado em uma determinada ação, ele ignora os filtros aplicados em nível global.

public class TestController : ApiController
{
    [HttpGet]
    public string Teste1()
    {
        return "teste1";
    }

    [HttpGet]
    [OverrideActionFilters]
    public string Teste2()
    {
        return "teste2";
    }
}

Além deste atributo, temos outros três com a mesma finalidade, ou seja, interromper a execução de determinados tipos de filtros que foram aplicados em nível global. Os outros atributos que temos para isso são: OverrideAuthenticationAttribute, OverrideAuthorizationAttribute e OverrideExceptionAttribute.

Configurações

Durante todos os capítulos vimos diversas configurações que são realizadas em nível global. Para todas elas recorremos ao objeto HttpConfiguration. Ele fornece diversas propriedades e métodos que nos permite interagir com todos os recursos que são utilizados durante a execução das APIs. Uma das propriedades que vale ressaltar é a Services do tipo ServicesContainer. Essa classe consiste em armazenar todos os recursos que são utilizados pelo ASP.NET Web API para fornecer ao runtime os responsáveis por executar cada tarefa específica, tais como: criação do controller, gestor de dependências, gestor de tracing, etc.

Dn376308.274807E0DCAA820458E68B42B7134555(pt-br,MSDN.10).png

Figura 28 - Classes para a customização das configurações.

A propriedade Services da classe ServicesContainer é inicializada com a instância da classe DefaultServices, que vale para todos e qualquer controller dentro da aplicação. Podemos variar certas configurações para cada controller, e é justamente para isso que temos a classe ControllerServices.

Se quisermos customizar a configuração por controller, onde cada um deles possui uma necessidade específica, basta implementarmos a interface IControllerConfiguration (namespace System.Web.Http.Controllers), onde através do método Initialize, realizamos todas as configurações específicas, e durante a execução o ASP.NET irá considerar essas configurações, sobrescrevendo as globais, e para aquelas que não alterarmos, a configuração global será utilizada.

public class ConfiguracaoPadrao : Attribute, IcontrollerConfiguration
{
    public void Initialize(
        HttpControllerSettings controllerSettings,
        HttpControllerDescriptor controllerDescriptor)
    {
        controllerSettings.Formatters.Add(new CsvMediaTypeFormatter());
    }
} 

[ConfiguracaoPadrao]
public class TestController : ApiController
{
    //ações
}

Veja também:

ASP.NET Web API – HTTP, REST e o ASP.NET: Para basear todas as funcionalidades expostas pela tecnologia, precisamos ter um conhecimento básico em relação ao que motivou tudo isso, contando um pouco da história e evolução, passando pela estrutura do protocolo HTTP e a relação que tudo isso tem com o ASP.NET.

ASP.NET Web API – Estrutura da API: Entenderemos aqui a template de projeto que o Visual Studio fornece para a construção das APIs, bem como sua estrutura e como ela se relaciona ao protocolo.

ASP.NET Web API – Roteamento: Como o próprio nome diz, o capítulo irá abordar a configuração necessária para que a requisição seja direcionada corretamente para o destino solicitado, preenchendo e validando os parâmetros que são por ele solicitado.

ASP.NET Web API – Hosting: Um capítulo de extrema relevância para a API. É o hosting que dá vida à API, disponibilizando para o consumo por parte dos clientes, e a sua escolha interfere diretamente em escalabilidade, distribuição e gerenciamento. Existem diversas formas de se expor as APIs, e aqui vamos abordar as principais delas.

ASP.NET Web API – Consumo: Como a proposta é ter uma API sendo consumido por qualquer cliente, podem haver os mais diversos meios (bibliotecas) de consumir estas APIs. Este capítulo tem a finalidade de exibir algumas opções que temos para este consumo, incluindo as opções que a Microsoft criou para que seja possível efetuar o consumo por aplicações .NET.

ASP.NET Web API – Formatadores: Os formatadores desempenham um papel importante na API. São eles os responsáveis por avaliar a requisição, extrair o seu conteúdo, e quando a resposta é devolvida ao cliente, ele entra em ação novamente para formatar o conteúdo no formato em que o cliente possa entender. Aqui vamos explorar os formatadores padrões que já estão embuitdos, bem como a criação de um novo.

ASP.NET Web API – Segurança: Como a grande maioria das aplicações, temos também que nos preocupar com a segurança das APIs. E quando falamos de aplicações distribuídas, além da autenticação e autorização, é necessário nos preocuparmos com a segurança das mensagens que são trocadas entre o cliente e o serviço. Este capítulo irá abordar algumas opções que temos disponíveis para tornar as APIs mais seguras.

ASP.NET Web API – Testes e Tracing: Para toda e qualquer aplicação, temos a necessidade de escrever testes para garantir que a mesma se comporte conforme o esperado. Isso não é diferentes com APIs Web. Aqui iremos abordar os recursos, incluindo a própria IDE, para a escrita, gerenciamento e execução dos testes.

ASP.NET Web API – Estensibilidade e Arquitetura: Mesmo que já tenhamos tudo o que precisamos para criar e consumir uma API no ASP.NET Web API, a customização de algum ponto sempre acaba sendo necessária, pois podemos criar mecanismos reutilizáveis, “externalizando-os” do processo de negócio em si. O ASP.NET Web API foi concebido com a estensibilidade em mente, e justamente por isso que existe um capítulo exclusivo para abordar esse assunto.

| Home | Artigos Técnicos | Comunidade